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:
- 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). - 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. - 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.
- 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:
Arrayof a specific element type (e.g.,Array<Number>).Objectwith named properties, where each property has a type (e.g.,{ a: Number, b: Boolean }).Unionof types (e.g.,Number | String).Intersectionof types (e.g.,T1 & T2- for simplicity, focus on intersection of object types).Functionwith parameter types and a return type (e.g.,(a: Number, b: String) => Boolean).
- Primitive types:
- 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.,
10infersNumber,"hello"infersString). - Array literals (e.g.,
[1, 2, 3]infersArray<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).
- Literal values (e.g.,
- 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 forNumbers,Strings, but notBooleanandNumber). - Function call argument matching.
Expected Behavior:
The simulator should provide functions to:
parseType(typeString: string): Type: Converts a string like"number | string[]"into your internalTyperepresentation.inferType(expression: any): Type: Infers the type of a given JavaScript value or expression.checkAssignment(targetType: Type, sourceType: Type): boolean: Determines if a value ofsourceTypecan be assigned to a variable oftargetType.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:
anytype: Should bypass most type checks.nullandundefined: 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
parseTypewill 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
Typeclass 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
anytype is a special case that effectively disables type checking. - Think about how to handle potential ambiguities in type inference (e.g.,
[1, "a"]could beArray<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.