Open-Source Wikis

/

TypeScript

/

Features

/

Declaration emit

microsoft/TypeScript

Declaration emit

How --declaration (and --composite) produce .d.ts files. This is the half of the compiler that exists so libraries can ship typed APIs to consumers without revealing implementation details.

Where declaration emit lives

Subsystem File Role
Transformer src/compiler/transformers/declarations.ts (3,000+ lines) Walks the typed tree and produces a declaration-only tree
Sub-helpers src/compiler/transformers/declarations/ Helpers used by declarations.ts
Type-to-syntax src/compiler/expressionToTypeNode.ts (~70k lines) Convert checker Type objects back into TypeNode AST
Checker hooks src/compiler/checker.ts getEmitResolver, nodeBuilder, typeToTypeNode
Emitter src/compiler/emitter.ts Prints the declaration tree as .d.ts text

Purpose

A .d.ts file should:

  • Expose the same type signature as the implementation, with no implementation details.
  • Be self-contained — every name referenced must resolve transitively from explicit imports/exports.
  • Be deterministic — the same input should always produce the same output text.
  • Catch portability problems early. Things like "you exported a private type" or "your inferred return type can't be expressed without re-exporting an internal symbol" become errors at declaration-emit time.

How it works

graph TD
    SF["Type-checked SourceFile"] --> Trans["transformDeclarations"]
    Trans --> Walk["walk top-level declarations"]
    Walk --> Decide{"Has explicit type?"}
    Decide -->|yes| Copy["copy type as-is"]
    Decide -->|no| Synth["ask checker.nodeBuilder for inferred type"]
    Synth --> ETN["expressionToTypeNode walks Type"]
    ETN --> TypeNode["TypeNode AST"]
    Copy --> TypeNode
    TypeNode --> Strip["strip implementation; keep modifiers, decorators"]
    Strip --> Decl["declaration tree"]
    Decl --> Emit["emitter prints as .d.ts"]
    Trans --> Errs["report declaration emit diagnostics"]

The transformer walks every top-level declaration in the source file. For each:

  1. Has an explicit type? Copy the type as-is, after rewriting any internal references.
  2. No explicit type? Ask the checker via nodeBuilder.typeToTypeNode to synthesise one. If the type can't be expressed in .d.ts form (e.g., it references a private symbol), emit a diagnostic.

The result is a declaration tree — a SourceFile whose body is only types, signatures, ambient declarations, and re-exports. The emitter prints this tree using the same printer as for .js, but in EmitHint.SourceFile mode that suppresses any remaining implementation.

nodeBuilder

createTypeChecker in checker.ts exposes nodeBuilder — a service that converts a Type into a TypeNode. It's used by:

  • Declaration emit (transformDeclarations).
  • The language service's quickInfo (hover) — type hints are produced via the same path.
  • Code fixes that synthesise type annotations.

expressionToTypeNode.ts is the implementation. It carries a NodeBuilderContext recording visited types (to avoid infinite recursion), the current source file (for relative naming), and a tracker (SymbolTracker) that records private-symbol leaks.

Diagnostic story

Declaration emit produces error code 4000-series. The most common:

  • TS4023: Exported variable '{0}' has or is using name '{1}' from external module '{2}' but cannot be named. A symbol from a non-imported module appears in an inferred type.
  • TS4060: Return type of exported function has or is using private name '{1}'. Inferred return references a non-exported symbol.
  • TS4082: Default export of the module has or is using private name '{0}'. Same idea for default exports.

These exist because .d.ts consumers can't see your implementation — they only see what you export. If your inferred shape mentions something they can't import, the .d.ts is broken.

The fix is usually to add an explicit return type or to export the helper symbol. The errors only surface when --declaration is on; otherwise the inferred type stays internal.

--isolatedDeclarations

A newer flag (TS 5.5) that requires every exported declaration to have an explicit type annotation. With --isolatedDeclarations, the declaration emitter never has to call into the checker's nodeBuilder — every output type comes directly from the source. This makes parallel and per-file declaration emit feasible (and is part of why the typescript-go rewrite is plausible).

--composite

Implies --declaration and --isolatedDeclarations-friendly behaviour, plus --incremental. Required for project references — downstream projects only see the upstream's .d.ts outputs.

Tests

Declaration-emit tests live across tests/cases/conformance/declarationEmit/ and many other conformance subdirectories. Each test produces a baseline .d.ts file (*.d.ts.txt) that's compared on every run. The team treats unintended baseline changes as regressions.

Entry points for modification

See systems/checker for the type machinery and systems/emitter for printing.

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

Declaration emit – TypeScript wiki | Factory