Open-Source Wikis

/

TypeScript

/

Systems

/

Binder

microsoft/TypeScript

Binder

The binder walks each parsed SourceFile and computes:

  1. Symbol tables for every container (modules, blocks, classes, functions).
  2. Flow nodes — a control-flow graph used by the checker for narrowing and reachability.
  3. Per-file metadata the checker depends on (commonJsModuleIndicator, externalModuleIndicator, bindDiagnostics, …).

It runs once per SourceFile, eagerly, before the checker sees the file.

Source

Single file: src/compiler/binder.ts (~3,900 lines).

Purpose

The parser produces an unannotated tree. The binder is the first pass that understands the tree as a program: it decides which declarations are alive in which scopes, merges interface declarations that share a name, classifies class members vs. instance members, and constructs the flow graph that powers if (typeof x === "string")-style narrowing.

The binder also detects the kind of file:

  • ES module (has export/import)
  • CommonJS-augmented JS (assigns to module.exports)
  • Script (no module syntax)
  • Plain .json import

Each detection sets a flag on the SourceFile that the program uses to drive module resolution and emit.

Key abstractions

Symbol Role
bindSourceFile(file, options) Public entry — binds one file. Idempotent: repeated calls no-op
Symbol, SymbolFlags The unit of declaration; see primitives/symbol
SymbolTable A Map<__String, Symbol> keyed on escaped names
FlowNode, FlowFlags Edges in the flow graph: assignments, branches, calls
Container (predicates canHaveLocals, canHaveSymbol, canHaveFlowNode) Nodes that own a SymbolTable and a flow graph
bindContainer The recursive walk
addToContainerChain Attaches symbol-bearing nodes to the active container

SymbolFlags is a packed bit-set — Variable | Function | Class | Interface | TypeAlias | Property | Method | …. Multiple bits can be combined when declarations merge (e.g., a class Foo and an interface Foo produce a Symbol with both flags).

How it works

graph TD
    Bind["bindSourceFile(file)"] --> Walk["bindEachChild"]
    Walk --> Decl{"Is declaration?"}
    Decl -->|yes| AddSym["declareSymbol → container.locals/exports/members"]
    Decl -->|no| Cont["Continue walking"]
    Walk --> Flow{"Is control-flow significant?"}
    Flow -->|yes| FN["createFlowNode → set node.flowNode"]
    Walk --> Diag["report bind diagnostics (duplicate identifier, etc.)"]
    Walk --> Detect["set externalModuleIndicator etc."]

Symbol declaration

declareSymbol is the workhorse. It walks up to the active container, looks up the name in the appropriate SymbolTable (locals for value scope, exports for module scope, members for class instance/static, etc.), and either creates a new Symbol or merges with an existing one. Merging is what makes interface/namespace/class merging work in TypeScript.

Flow graph

Every "interesting" expression — if, while, &&, ||, switch, ternary, assignment, function call, type guard — gets a FlowNode describing how control reaches it. The checker walks this graph backwards from a use site to compute the narrowed type at that point. Flow nodes are stored on the AST nodes themselves (node.flowNode) and survive incremental edits as long as the surrounding tree is reused.

Module / script detection

The binder watches for import, export, and CommonJS module.exports = ... patterns. Setting externalModuleIndicator flips a SourceFile from script to module mode, which changes how the program treats top-level declarations (private vs. exported) and which lib globals are visible.

commonJsModuleIndicator and assignmentDeclarationKind

For --allowJs, the binder recognises common JS idioms that look like declarations: Foo.prototype.bar = function () {}, module.exports.foo = ..., exports.bar = ..., Foo.X = 0. Each pattern is given an AssignmentDeclarationKind so the checker can synthesise types for them.

Diagnostics

The binder emits a small but important set of diagnostics:

  • Duplicate identifier
  • Cannot redeclare block-scoped variable
  • 'this' implicitly has type 'any' because it does not have a type annotation
  • 'super' must be called before accessing 'this' in the constructor of a derived class
  • A handful of with and reserved-word errors

These appear on SourceFile.bindDiagnostics. Most type errors come later in the checker.

Integration points

  • Called from src/compiler/program.ts on demand — the program lazily binds files when the checker first asks for them.
  • Reads CompilerOptions.target, CompilerOptions.module, CompilerOptions.allowJs to decide JS-aware behaviours.
  • Outputs are consumed almost entirely by src/compiler/checker.ts.
  • The flow graph is used by control-flow analysis (search for getFlowTypeOfReference in checker.ts).

Entry points for modification

If you're touching the binder you almost certainly are:

  • Adding a new declaration kind — extend declareSymbol to recognise it, give it a SymbolFlags value, and ensure nodeTests.ts predicates classify it correctly.
  • Adding a new flow construct — add a FlowFlags value, build the flow node where the construct is bound, and teach getFlowTypeOfReference in checker.ts to read it.
  • Adding a new JS assignment idiom — extend getAssignmentDeclarationKind and bindCommonJsModuleAssignment / siblings.

See systems/checker for what consumes the binder's output, and primitives/symbol for the full Symbol model.

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

Binder – TypeScript wiki | Factory