This page documents the transformer architecture that converts a type-checked TypeScript AST into an output-ready JavaScript AST. The transformation pipeline runs after type checking (see page 2.3) and before the emitter prints text output (see page 2.4).
The pipeline handles two categories of work:
The entry point is transformNodes in src/compiler/transformer.ts248-357 It creates a TransformationContext, initializes each transformer by calling its factory, then passes each source file through every transformer in sequence.
Diagram: Transformer Orchestration
Sources: src/compiler/transformer.ts248-357 src/compiler/transformer.ts120-197
TransformerFactory and TransformerEvery built-in and custom transformer follows this shape:
TransformerFactory<T> = (context: TransformationContext) => Transformer<T>
Transformer<T> = (node: T) => T
A factory is called once per transformation pass to receive the TransformationContext. It returns a Transformer function that is called on each root node (typically SourceFile or Bundle).
TransformationContextTransformationContext is the shared interface provided to every transformer. It is constructed once inside transformNodes and passed to all factories. Its most important members:
| Member | Description |
|---|---|
factory | NodeFactory — creates and updates AST nodes |
getCompilerOptions() | Compiler options for the current build |
getEmitResolver() | Semantic query interface from the type checker |
getEmitHelperFactory() | Factory for emit helper expressions |
startLexicalEnvironment() | Begin collecting hoisted declarations |
endLexicalEnvironment() | Flush hoisted declarations as statements |
hoistVariableDeclaration(name) | Hoist a var to the enclosing function scope |
hoistFunctionDeclaration(func) | Hoist a function declaration to the enclosing scope |
enableSubstitution(kind) | Register a SyntaxKind for just-in-time substitution during printing |
enableEmitNotification(kind) | Register a SyntaxKind for before/after emit callbacks |
onSubstituteNode | Callback invoked by the printer to substitute a node at print time |
onEmitNode | Callback invoked by the printer around a node's emission |
readEmitHelpers() | Retrieve all accumulated emit helpers for attachment to the file |
Sources: src/compiler/transformer.ts271-314
NodeFactoryNodeFactory (defined in src/compiler/factory/nodeFactory.ts) provides:
createXxx(...) methods to construct new AST nodes.updateXxx(original, ...) methods that return the original node if no arguments differ, otherwise a new node with original set as its .original property (preserving source map linkage).Each transformer destructures what it needs from context:
const { factory, getEmitHelperFactory: emitHelpers, startLexicalEnvironment, endLexicalEnvironment, hoistVariableDeclaration } = context;
Sources: src/compiler/transformers/ts.ts236-243 src/compiler/transformers/es2015.ts488-496
Transformers navigate and transform the AST using a set of visitor utilities:
| Function | Description |
|---|---|
visitNode(node, visitor, test?) | Visit a single node; returns the transformed node or undefined |
visitNodes(nodes, visitor, test?, start?, count?) | Visit a NodeArray; returns a new array if any node changes |
visitEachChild(node, visitor, context) | Visit every child of a node using the appropriate child-visiting logic |
visitFunctionBody(body, visitor, context) | Visit a function body inside a fresh lexical environment |
visitParameterList(params, visitor, context) | Visit parameters inside a parameter-scoped lexical environment |
visitLexicalEnvironment(stmts, visitor, context) | Visit a statement list, then append hoisted declarations |
The return type of a visitor is VisitResult<Node | undefined>, which can be a single node, an array of nodes (statement expansion), or undefined (elision).
A typical transformer uses TransformFlags on each node to fast-path through subtrees that contain no relevant syntax:
Sources: src/compiler/transformers/ts.ts382-396 src/compiler/transformers/es2015.ts593-603
Every Node carries a transformFlags: TransformFlags bitmask that is computed during parsing (by the binder or parser) and propagated up the tree. Transformers use these to avoid descending into subtrees that contain no syntax they care about.
| Flag | Meaning |
|---|---|
ContainsTypeScript | Subtree contains TypeScript-only syntax |
ContainsTypeScriptClassSyntax | Subtree contains TS class features (decorators, parameter properties) |
ContainsES2015 | Subtree contains ES2015 syntax (classes, arrow functions, destructuring, etc.) |
ContainsES2016 through ContainsES2021 | Syntax introduced in each respective ECMAScript edition |
ContainsESNext | Subtree contains ESNext syntax (e.g., using declarations) |
ContainsDynamicImport | Contains a dynamic import() call |
ContainsHoistedDeclarationOrCompletion | Contains a var or hoistable control flow |
Sources: src/compiler/transformers/ts.ts391-396 src/compiler/transformers/es2015.ts593-598
When a transformer introduces new var declarations or function declarations into a scope (e.g., temporary variables for destructuring), it uses the lexical environment API:
startLexicalEnvironment() — push a new frame onto the hoisting stack.hoistVariableDeclaration(name) / hoistFunctionDeclaration(func) — accumulate declarations in the current frame.endLexicalEnvironment() — pop the frame and return the accumulated declarations as Statement[].insertStatementsAfterStandardPrologue(statements, env) — insert the accumulated statements after any "use strict" prologue directives.This is used extensively in transformCommonJSModule and transformES2015.
Diagram: Lexical Environment Stack
Sources: src/compiler/transformer.ts431-512 src/compiler/transformers/module/module.ts276-316
Emit helpers are reusable runtime functions injected into the output (e.g., __awaiter, __generator, __extends). They are accessed through the emit helper factory returned by context.getEmitHelperFactory().
Each transformer calls context.requestEmitHelper(helper) (or through the helper factory) when it needs a helper. After transformation, context.readEmitHelpers() retrieves all requested helpers, which are then attached to the SourceFile node via addEmitHelpers(sourceFile, helpers). The printer emits them at the top of each file.
Helpers may be:
__extends, __awaiter, __generator).__read, __spread).When --importHelpers is set, the helpers are replaced with imports from tslib instead of inline definitions.
Sources: src/compiler/transformers/ts.ts310-314 src/compiler/transformers/es2015.ts539-547 src/compiler/transformers/generators.ts336-345
Some transformers need to alter identifiers or expressions at print time rather than during the AST walk (e.g., replacing a namespace-qualified name or an enum member reference with its constant value).
This is handled via two hooks on TransformationContext:
onSubstituteNode(hint, node) — called by the printer before emitting a node of a kind that has been registered with enableSubstitution(kind). Returns a replacement node.onEmitNode(hint, node, emitCallback) — called by the printer around the emission of a registered node kind. Can push/pop state before/after the callback.Transformers chain these hooks by saving the previous value and composing:
const previousOnSubstituteNode = context.onSubstituteNode;
context.onSubstituteNode = (hint, node) => { /* check; delegate to previous */ };
For example, the TypeScript transformer uses substitution to inline const enum values at property/element access sites. The module transformer uses substitution to rewrite exported variable assignments into exports.X = ....
Sources: src/compiler/transformer.ts363-426 src/compiler/transformers/ts.ts252-262 src/compiler/transformers/module/module.ts205-214
getScriptTransformers in src/compiler/transformer.ts127-190 assembles the ordered transformer list based on CompilerOptions. Each transformer is added only when its output is needed.
Diagram: Script Transformer Selection
Sources: src/compiler/transformer.ts127-190
Declaration files go through a separate path: getDeclarationTransformers always includes transformDeclarations, with optional custom afterDeclarations transformers appended.
transformTypeScript — src/compiler/transformers/ts.tsPurpose: Erase all TypeScript-specific syntax that has no JavaScript equivalent.
What it removes:
:string, :number, etc.)abstract, readonly, declare, override, public, private, protected)x as T, <T>x), satisfies expressions, non-null assertions (x!)const enum declarations (inlined as literal values)namespace / module declarations (rewritten to IIFEs)experimentalDecorators-style legacy decorator syntax (working with transformLegacyDecorators)Key internal visitors:
| Visitor | Handles |
|---|---|
visitorWorker | General branching; delegates to visitTypeScript if ContainsTypeScript is set |
visitTypeScript | Large switch on node.kind; handles each TS syntax kind |
sourceElementVisitor | Top-level statements; handles import/export elision |
classElementVisitorWorker | Class members; handles parameter properties, decorators |
modifierVisitor | Strips TS-only modifiers |
Sources: src/compiler/transformers/ts.ts382-836
transformES2015 — src/compiler/transformers/es2015.tsPurpose: Downlevel ES2015 syntax to ES5-compatible JavaScript.
Features handled:
| Syntax | Output |
|---|---|
| Arrow functions | Regular function expressions capturing this via _this alias |
class declarations/expressions | Prototype-based constructor functions (when targeting < ES2015) |
| Template literals | String concatenation |
| Destructuring parameters and variables | Sequential assignment expressions |
| Default parameters | === void 0 guards |
| Rest parameters | Array.prototype.slice |
| Spread in calls/arrays | .apply() / __spread helper |
for...of loops | Iterator protocol calls or index-based loops |
| Computed property names | IIFE-based object construction |
Block-scoped let/const (in loop closures) | Wrapped in IIFE with out-parameters |
| Shorthand object properties | Explicit key: value form |
super in class methods | _super alias |
Generators (function*) | Handled by transformGenerators |
State is tracked using the HierarchyFacts enum, which records facts about the current ancestry (e.g., ArrowFunction, ConstructorWithSuperCall, IterationStatement) to guide transformation decisions.
Sources: src/compiler/transformers/es2015.ts371-469 src/compiler/transformers/es2015.ts487-529
transformGenerators — src/compiler/transformers/generators.tsPurpose: Transform generator functions (function*) into ES5-compatible state machines when targeting ES5.
Generator function bodies are compiled to a __generator(function(state) { switch(state.label) { ... } }) pattern. The transformer internally uses an intermediate representation with labeled operations (.nop, .br, .yield, .try, .catch, .finally, .return) before emitting the switch-based output.
Sources: src/compiler/transformers/generators.ts100-234
transformES2017 — src/compiler/transformers/es2017.tsPurpose: Downlevel async/await functions.
Async functions are rewritten to use the __awaiter and __generator emit helpers. async function becomes a wrapper that calls __awaiter(this, void 0, void 0, function* () { ... }). The await keyword becomes yield. super property accesses inside async methods require special handling via substitution.
Sources: src/compiler/transformers/es2017.ts119-178
transformESNext — src/compiler/transformers/esnext.tsPurpose: Downlevel ESNext syntax not yet stabilized into a lower target.
Currently handles using and await using declarations (Explicit Resource Management proposal). These are lowered to try/finally blocks using the __addDisposableResource and __disposeResources emit helpers. When using appears at the top level of a module source file, imports/exports and function declarations must be hoisted out of the generated try block.
Sources: src/compiler/transformers/esnext.ts71-234
getModuleTransformer(moduleKind) in src/compiler/transformer.ts78-102 selects the right module transformer based on the --module compiler option:
| Module Kind | Transformer |
|---|---|
CommonJS, Node16, NodeNext, ES2015–ESNext, Node18, Node20 | transformImpliedNodeFormatDependentModule (wraps transformModule + transformECMAScriptModule) |
AMD, UMD | transformModule |
System | transformSystemModule |
Preserve | transformECMAScriptModule |
transformModule (src/compiler/transformers/module/module.ts) handles CommonJS, AMD, and UMD output:
import to require() calls and export to exports.X = ... assignments.define([...deps], function(...) { ... }) call.transformSystemModule (src/compiler/transformers/module/system.ts) wraps the file in System.register(['dep1', ...], function(exports, context) { return { setters: [...], execute: function() { ... } }; }).
Sources: src/compiler/transformer.ts78-102 src/compiler/transformers/module/module.ts174-259 src/compiler/transformers/module/system.ts136-272
Diagram: Module Transformer Selection by moduleKind
Sources: src/compiler/transformer.ts78-102
transformDeclarationsThe declaration transformer runs on a separate pass (not part of the script transformer chain). It produces .d.ts output by stripping all function bodies, initializers, and private implementation details, retaining only the public API surface as type declarations. It is always enabled when declaration: true or composite: true.
Sources: src/compiler/transformer.ts192-197
Destructuring transformations are shared across multiple transformers (transformES2015, transformES2018) via two utility functions in src/compiler/transformers/destructuring.ts:
flattenDestructuringAssignment — converts a destructuring assignment expression into a sequence of simple assignments.flattenDestructuringBinding — converts a destructuring variable declaration into multiple var/let/const declarations.Both accept a FlattenLevel argument:
FlattenLevel.All — flatten all destructuring patterns.FlattenLevel.ObjectRest — flatten only patterns involving object rest (...rest).Sources: src/compiler/transformers/destructuring.ts80-195 src/compiler/transformers/destructuring.ts244-300
The public API (CustomTransformers) allows users to inject transformers at three positions:
| Position | Property | Runs |
|---|---|---|
| Before built-ins | before: TransformerFactory<SourceFile>[] | Before transformTypeScript |
| After built-ins | after: TransformerFactory<SourceFile>[] | After module transformer |
| After declaration emit | afterDeclarations: TransformerFactory<SourceFile | Bundle>[] | After transformDeclarations |
Custom transformer factories receive the same TransformationContext as built-in transformers and can call context.enableSubstitution, context.hoistVariableDeclaration, etc.
Custom transformers returning an object with transformSourceFile and transformBundle methods (CustomTransformer) are wrapped automatically by wrapCustomTransformer to adapt them to the Transformer<Bundle | SourceFile> signature expected internally.
Sources: src/compiler/transformer.ts199-224 src/compiler/transformer.ts127-140 src/compiler/transformer.ts187-197
The TransformationState enum in src/compiler/transformer.ts104-109 enforces this lifecycle. Modifying onSubstituteNode or onEmitNode after the Initialized state throws a debug assertion. The lexical environment cannot be modified after Completed.
Sources: src/compiler/transformer.ts104-109 src/compiler/transformer.ts295-312 src/compiler/transformer.ts343-356
The emitter (see page 2.4) receives a TransformationResult<SourceFile> from transformNodes. During printing, it calls back into the transformation result through:
substituteNode(hint, node) — to apply onSubstituteNode substitutions for registered syntax kinds.emitNodeWithNotification(hint, node, callback) — to fire onEmitNode hooks around registered node kinds.This late-binding substitution mechanism allows transformers to perform context-sensitive rewrites (e.g., replacing an identifier that refers to a namespace member with a qualified name like NS.x) without requiring a second tree walk.
Sources: src/compiler/transformer.ts383-426
Refresh this wiki
This wiki was recently refreshed. Please wait 4 days to refresh again.