Open-Source Wikis

/

TypeScript

/

Systems

/

Type checker

microsoft/TypeScript

Type checker

The largest and most consequential subsystem in the compiler. The checker answers all type questions: it builds Type objects, computes assignability, runs control-flow narrowing, infers generic type arguments, resolves overloads, and produces the long tail of TypeScript-specific diagnostics.

Source

src/compiler/checker.ts54,434 lines in a single file. Roughly 12% of all TS source in the repository. Internally organised by section comments rather than by export structure.

Purpose

The checker has three rough jobs that are interleaved across its surface:

  1. Lookup — given an AST node, find its symbol, then the symbol's type.
  2. Inference — run type inference for generics, contextual typing, and --noImplicitAny defaulting.
  3. Validation — emit diagnostics when inferred types fail assignability, when --strictNullChecks rules are violated, when overloads are ambiguous, etc.

It also acts as the resolution oracle for downstream tools — the language service, the emitter, and the declaration-emit pipeline all call into it.

Key abstractions

Symbol Role
createTypeChecker(host, produceDiagnostics) Factory; returns a fully-configured TypeChecker
TypeChecker (interface in types.ts) The public surface used by callers
Type, TypeFlags, ObjectType, UnionType, IntersectionType, IndexedAccessType, MappedType, ConditionalType, … Type representations; see primitives/type
Signature Function/constructor signature; see primitives/signature
getSymbolAtLocation(node) Resolve a node to its Symbol
getTypeOfSymbol(symbol) Compute (and cache) the type of a symbol
getTypeAtLocation(node) Compute the contextual type at a location
getApparentType(type) Apply intersection-with-base for primitives, etc.
isTypeAssignableTo(source, target) Public assignability
checkSourceFile(file) Drives full-file checking

Architecture

graph LR
    Host["TypeCheckerHost (Program)"] --> Checker["createTypeChecker"]
    Checker --> Resolve["resolveName / resolveSymbol"]
    Checker --> SymTypes["getTypeOfSymbol (cache: symbol.type)"]
    Checker --> Assign["checkTypeAssignableTo"]
    Checker --> CFA["getFlowTypeOfReference"]
    Checker --> Infer["inferTypes / inferFromTypes"]
    Checker --> Resolution["resolveCall / chooseOverload"]
    Checker --> Emit["getEmitResolver (for emitter)"]
    Checker --> Diag["error + addDiagnostic"]
    Diag --> ProgramDiag["Program.getSemanticDiagnostics"]

The checker holds a TypeCheckerHost (the program) so it can ask for source files and bind them on demand. Internally it caches:

  • symbol.id → Type once getTypeOfSymbol resolves it.
  • Type instances by structural shape so identical types are shared (flyweight pattern).
  • Per-call inference results.

Lazy evaluation

The checker does not pre-compute types for every symbol. Instead it answers questions on demand and memoises the results. This is what makes the language service responsive — opening one file doesn't force checking unrelated parts of the program.

Control-flow analysis

CFA is the part of the checker that turns

function f(x: string | number) {
  if (typeof x === 'string') {
    x.toUpperCase(); // x: string here
  }
}

into something the rest of the checker can reason about. It walks FlowNodes built by the binder backwards from the reference site. Each flow node represents a control-flow joint (assignment, branch, call), and the walker accumulates narrowing predicates along the way. CFA also drives definite-assignment analysis (--strictPropertyInitialization), unreachable-code detection, and never propagation.

Inference

inferTypes(context, source, target, priority) is the workhorse. Given a target type like Array<T> and a source type like string[], it walks both in parallel, recording inferences for T. Inference handles:

  • Conditional-type distribution (infer T clauses)
  • Variance (covariant, contravariant, bivariant)
  • Higher-order inference for callbacks
  • Contextual typing — propagating expected types into expression trees so e.g. x.map(item => item.foo) knows item's type

Overload resolution

resolveCall chooses among overloaded signatures. It tries each candidate, accumulates errors, and picks the most specific match — or, if none match, reports a diagnostic against the closest candidate. This is one of the heaviest paths in the checker because each candidate's inference has its own type arguments and assignability checks.

Diagnostics

User-visible errors, warnings, and messages are queued via error / errorOrSuggestion / addDiagnostic. They reference entries in src/compiler/diagnosticMessages.json so they can be localised. The checker also produces suggestion diagnostics (e.g., "Did you mean to mark this method async?") that only the language service surfaces.

Integration points

  • Program (program.ts) creates exactly one checker per program via createTypeChecker(host, /*produceDiagnostics*/ true).
  • Language service (src/services/) uses the checker for completions, quick info, signature help, find-references, refactorings — basically every IDE feature.
  • Emitter asks the checker for an EmitResolver (a stripped-down API for emit-time questions like "is this name exported?", "what's the resolved external module?").
  • Declaration emit (transformers/declarations.ts) calls the checker to resolve nominal references to their declarations and produce minimal .d.ts shapes.

Performance

Because of the file's size, performance changes are noisy. The team treats certain patterns as load-bearing:

  • Plain for loops over arrays (faster than for…of on V8 fast paths).
  • Object.create(null)-style maps (MapLike) instead of native Map for short-lived lookups.
  • Flyweighting types so equality is reference equality.
  • A Debug.assertDefined discipline so error paths don't deopt into undefined-handling.

The tracing.ts module ships hooks that emit Chrome-trace events when --generateTrace is set; the team uses them to diagnose checker hotspots.

Entry points for modification

There is rarely a "small" change to checker.ts. Even a single-line fix usually requires:

  1. Identifying the right entry point (getTypeOfX, checkX, isTypeAssignableTo, etc.).
  2. Writing a compiler test under tests/cases/compiler/ or tests/cases/conformance/ that fails before the change.
  3. Producing baselines via hereby runtests --tests=… and inspecting the diffs.
  4. Sweeping the rest of the suite with hereby runtests-parallel because the checker is exercised by many tests.

The team's coding-guideline document (linked from CONTRIBUTING.md) covers more checker-specific conventions.

For the data structures the checker manipulates, see primitives/type, primitives/symbol, primitives/signature. For who calls into the checker, see systems/program and systems/language-service.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Type checker – TypeScript wiki | Factory