Skip to main content

The Type Checker

The type checker is the largest and most complex component of the TypeScript compiler. Located in src/compiler/checker.ts (over 3.1 MB), it performs semantic analysis and type validation.

Creating the Type Checker

// From src/compiler/checker.ts (line 1486)
export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
    // Deferred diagnostics for lazy evaluation
    var deferredDiagnosticsCallbacks: (() => void)[] = [];

    var addLazyDiagnostic = (arg: () => void) => {
        deferredDiagnosticsCallbacks.push(arg);
    };

    // Type checking proceeds lazily to improve performance
    // ...
}
The type checker uses var instead of let/const to avoid Temporal Dead Zone (TDZ) runtime checks, improving performance. See TypeScript Issue #52924.

Type Checker Responsibilities

The checker performs multiple critical functions:

Type Inference

Determines types of expressions without explicit annotations

Type Compatibility

Checks if one type can be assigned to another

Signature Resolution

Resolves overloaded function calls

Flow Analysis

Tracks type narrowing through control flow

How Type Checking Works

Step 1: Symbol Resolution

The checker first resolves all symbols created by the binder:
// Example from checker.ts
function getTypeOfSymbol(symbol: Symbol): Type {
    // Resolve the type associated with a symbol
    if (symbol.flags & SymbolFlags.Variable) {
        return getTypeOfVariableOrParameterOrProperty(symbol);
    }
    if (symbol.flags & SymbolFlags.Function) {
        return getTypeOfFuncClassEnumModule(symbol);
    }
    // ... many more cases
}

Step 2: Type Computation

Types are computed recursively from AST nodes:
// Simplified example of type checking process
function getTypeFromTypeNode(node: TypeNode): Type {
    switch (node.kind) {
        case SyntaxKind.StringKeyword:
            return stringType;
        case SyntaxKind.NumberKeyword:
            return numberType;
        case SyntaxKind.TypeReference:
            return getTypeFromTypeReference(node);
        case SyntaxKind.UnionType:
            return getUnionType(map(node.types, getTypeFromTypeNode));
        // ... hundreds more cases
    }
}

Step 3: Compatibility Checking

The checker validates assignments using structural type comparison:
// Type compatibility example
interface Point { x: number; y: number; }
interface Named { name: string; }

let point: Point = { x: 0, y: 0 };
let named: Named = { name: "origin" };

// The checker validates this assignment
point = { x: 1, y: 2 }; // ✓ Compatible

// And rejects this one
point = named; // ✗ Error: Types have no properties in common

Type System Features

Structural Typing

TypeScript uses structural (duck) typing:
interface Drawable {
    draw(): void;
}

class Circle {
    draw() { console.log("Drawing circle"); }
}

class Square {
    draw() { console.log("Drawing square"); }
}

// Both are compatible with Drawable
const drawable: Drawable = new Circle(); // ✓
const drawable2: Drawable = new Square(); // ✓
The checker determines compatibility by comparing the structure (shape) of types, not their names.

Union Types

The checker handles union types by ensuring operations are valid on all constituents:
function processValue(value: string | number) {
    // Checker allows operations valid on both types
    console.log(value.toString()); // ✓
    
    // Checker rejects operations not valid on all types
    console.log(value.toUpperCase()); // ✗ Error: Property 'toUpperCase' does not exist on type 'number'
}

Type Narrowing

Control flow analysis narrows types within conditional branches:
function example(x: string | number) {
    if (typeof x === "string") {
        // Type narrowed to string
        console.log(x.toUpperCase()); // ✓
    } else {
        // Type narrowed to number
        console.log(x.toFixed(2)); // ✓
    }
}

Generic Types

The checker performs type parameter substitution:
function identity<T>(value: T): T {
    return value;
}

// Checker infers T = number
const num = identity(42);

// Checker infers T = string
const str = identity("hello");

Type Checking Algorithm

1

Get Type of Source

Determine the type of the source expression
const source = "hello"; // Type: string
2

Get Type of Target

Determine the type of the target
let target: string | number; // Type: string | number
3

Check Assignability

Verify source type is assignable to target type
target = source; // ✓ string is assignable to string | number
4

Report Diagnostics

Generate errors for incompatible assignments
let num: number;
num = source; // ✗ Error: Type 'string' is not assignable to type 'number'

Diagnostic Generation

The checker creates detailed error messages:
// From checker.ts - diagnostic creation
function createDiagnosticForNode(
    node: Node,
    message: DiagnosticMessage,
    ...args: DiagnosticArguments
): DiagnosticWithLocation {
    const sourceFile = getSourceFileOfNode(node);
    const span = getErrorSpanForNode(sourceFile, node);
    return createFileDiagnostic(
        sourceFile,
        span.start,
        span.length,
        message,
        ...args
    );
}

Diagnostic Categories

let num: number = "hello";
// Error: Type 'string' is not assignable to type 'number'

Performance Optimizations

The type checker employs several optimization strategies:

Lazy Evaluation

Type checking is deferred until needed, avoiding work on unused code paths.
// From checker.ts
var addLazyDiagnostic = (arg: () => void) => {
    deferredDiagnosticsCallbacks.push(arg);
};

Type Caching

Types are computed once and cached:
// Computed types are stored on symbols
interface Symbol {
    type?: Type; // Cached type
}

Signature Caching

Resolved function signatures are cached to avoid recomputation:
function getResolvedSignature(
    node: CallLikeExpression,
    candidatesOutArray?: Signature[],
    checkMode?: CheckMode
): Signature {
    // Check cache first
    // Compute and cache if not found
}

Internal Type Representations

Type Flags

enum TypeFlags {
    Any = 1 << 0,
    String = 1 << 1,
    Number = 1 << 2,
    Boolean = 1 << 3,
    Enum = 1 << 4,
    Void = 1 << 5,
    Undefined = 1 << 6,
    Null = 1 << 7,
    // ... many more
}

Type Hierarchy

interface Type {
    flags: TypeFlags;
    symbol?: Symbol;
}

interface ObjectType extends Type {
    objectFlags: ObjectFlags;
}

interface InterfaceType extends ObjectType {
    typeParameters: TypeParameter[] | undefined;
}

Advanced Features

The checker evaluates conditional types by checking constraints:
type IsString<T> = T extends string ? true : false;

type A = IsString<"hello">; // true
type B = IsString<42>;      // false
The checker transforms types by mapping over properties:
type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};
The checker manipulates string literal types:
type Greeting<T extends string> = `Hello ${T}`;

type Welcome = Greeting<"World">; // "Hello World"
The checker tracks covariance and contravariance:
interface Producer<out T> { // Covariant
    produce(): T;
}

interface Consumer<in T> { // Contravariant
    consume(value: T): void;
}

Compiler Overview

Learn about the full compilation pipeline

Module Resolution

Understand how imports are resolved

Build docs developers (and LLMs) love