Hone logo
Hone
Problems

TypeScript Type System Simulator

This challenge asks you to build a simplified simulation of TypeScript's static type system. You will create a system that can parse, represent, and infer types for basic code constructs. This is a foundational exercise for understanding compiler design and type theory, crucial for building robust and maintainable software.

Problem Description

Your goal is to implement a type system simulator in TypeScript. This simulator should be able to:

  1. Represent Types: Define a way to represent various primitive and complex types (e.g., number, string, boolean, null, undefined, any, arrays, objects, unions, intersections, functions).
  2. Parse Type Annotations: Create a parser that can take type annotations (e.g., string[], { a: number, b: boolean }, number | string) and convert them into your internal type representations.
  3. Type Inference: Implement a mechanism to infer the type of expressions and variables when type annotations are not explicitly provided. This involves analyzing the values and operations within the code.
  4. Type Checking: Develop a type checker that can verify if operations and assignments are type-safe according to your inferred and/or annotated types.

Key Requirements:

  • Type Representation: Your system must be able to represent at least the following:
    • Primitive types: Number, String, Boolean, Null, Undefined, Any.
    • Complex types:
      • Array of a specific element type (e.g., Array<Number>).
      • Object with named properties, where each property has a type (e.g., { a: Number, b: Boolean }).
      • Union of types (e.g., Number | String).
      • Intersection of types (e.g., T1 & T2 - for simplicity, focus on intersection of object types).
      • Function with parameter types and a return type (e.g., (a: Number, b: String) => Boolean).
  • Parsing: Implement a simple parser that can interpret string representations of types. You don't need a full-fledged parser generator; manual parsing for the specified formats is acceptable.
  • Inference: Implement inference rules for common scenarios:
    • Literal values (e.g., 10 infers Number, "hello" infers String).
    • Array literals (e.g., [1, 2, 3] infers Array<Number>).
    • Object literals (e.g., { x: 1, y: "test" } infers { x: Number, y: String }).
    • Function calls (basic inference based on known function signatures).
    • Assignments (inferring the type of the right-hand side for variable declarations without explicit types).
  • Type Checking: Implement rules for:
    • Assignment compatibility (can a value of type A be assigned to a variable of type B?).
    • Operator type safety (e.g., + is valid for Numbers, Strings, but not Boolean and Number).
    • Function call argument matching.

Expected Behavior:

The simulator should provide functions to:

  • parseType(typeString: string): Type: Converts a string like "number | string[]" into your internal Type representation.
  • inferType(expression: any): Type: Infers the type of a given JavaScript value or expression.
  • checkAssignment(targetType: Type, sourceType: Type): boolean: Determines if a value of sourceType can be assigned to a variable of targetType.
  • checkOperation(operator: string, operands: Type[]): Type | Error: Checks the type compatibility of an operation and returns the resulting type or an error.

Edge Cases to Consider:

  • any type: Should bypass most type checks.
  • null and undefined: Their distinct types and how they interact with unions.
  • Empty arrays and objects: How to infer their types or if they should have a generic type.
  • Recursive types (optional, but a good extension).

Examples

Example 1: Basic Type Parsing and Inference

// Assume `Number`, `String`, `Boolean`, `Array`, `Object`, `Union` are your internal Type classes

// Parsing
const stringArrayType = parseType("string[]");
// Expected: stringArrayType should be an instance of ArrayType with elementType being StringType

const unionType = parseType("number | boolean");
// Expected: unionType should be an instance of UnionType with types NumberType and BooleanType

// Inference
const inferredNumber = inferType(123);
// Expected: inferredNumber should be NumberType

const inferredArray = inferType([1, "a", true]);
// Expected: inferredArray should be ArrayType with elementType being UnionType<[NumberType, StringType, BooleanType]>

const inferredObject = inferType({ name: "Hone", age: 30 });
// Expected: inferredObject should be ObjectType with properties { name: StringType, age: NumberType }

Example 2: Type Checking Operations and Assignments

// Assume `NumberType`, `StringType`, `BooleanType`, `ArrayType<StringType>` are defined

// Assignment Checking
const canAssignNumberToString = checkAssignment(StringType, NumberType);
// Expected: false

const canAssignStringToArrayOfStrings = checkAssignment(new ArrayType(StringType), StringType);
// Expected: true (for element assignment)

// Operation Checking
const resultOfNumberAddition = checkOperation("+", [NumberType, NumberType]);
// Expected: NumberType

const resultOfStringConcatenation = checkOperation("+", [StringType, StringType]);
// Expected: StringType

const resultOfBooleanAddition = checkOperation("+", [BooleanType, NumberType]);
// Expected: Error("Operator '+' cannot be applied to types 'boolean' and 'number'.")

Example 3: Function Type Checking

// Assume `FunctionType` and `StringType`, `NumberType` are defined

const addFunctionType = new FunctionType([NumberType, NumberType], NumberType);
const greetFunctionType = new FunctionType([StringType], StringType);

// Simulating function calls (simplified check)
const callResult1 = checkOperation("call", [addFunctionType, NumberType, NumberType]);
// Expected: NumberType

const callResult2 = checkOperation("call", [greetFunctionType, StringType]);
// Expected: StringType

const invalidCall1 = checkOperation("call", [addFunctionType, NumberType, StringType]);
// Expected: Error("Function expects arguments of type 'number, number', but received 'number, string'.")

Constraints

  • The type system should handle at least the types listed in the "Key Requirements" section.
  • Input type strings for parseType will follow a simple, well-defined grammar (e.g., type, type[], (type, type, ...) => type, { prop: type, ... }, type1 | type2). You can define this grammar.
  • Inference should cover basic literals, array/object literals, and variable declarations.
  • Type checking should cover assignment compatibility and basic arithmetic/concatenation operators.
  • Performance is not a primary concern for this challenge, but the solution should be reasonably efficient for typical inputs.

Notes

  • You'll likely want to define a base Type class or interface and then create subclasses for each type (e.g., PrimitiveType, ArrayType, ObjectType, UnionType, FunctionType).
  • Consider how to represent and manage generic types (e.g., Array<T>). For this challenge, you might start with concrete types and then explore generics.
  • The any type is a special case that effectively disables type checking.
  • Think about how to handle potential ambiguities in type inference (e.g., [1, "a"] could be Array<number | string> or something more complex). For this challenge, aim for a sensible default.
  • This is a simulation, so you are not bound by JavaScript's runtime behavior directly, but rather by the rules of static typing you define.
  • Success looks like a robust set of classes and functions that accurately simulate the described type checking and inference behaviors for a reasonable subset of TypeScript's features.
Loading editor...
typescript