microsoft/TypeScript
Patterns and conventions
The TypeScript compiler is more than a million lines of TypeScript-on-Node code accreted across a decade. Several stylistic and architectural conventions are unwritten elsewhere and only enforced by review. This page captures them.
Source layout
- All TypeScript sources live under
src/. The flat layout there is intentional: each top-level directory builds an independent project (compiler,services,server,tsc,tsserver,harness,testRunner,typingsInstaller,typingsInstallerCore,jsTyping,watchGuard,deprecatedCompat,lib,loc,typescript). - Each project directory has a
tsconfig.jsonthat extendssrc/tsconfig-base.json. - A project's public symbols are re-exported from a generated
_namespaces/barrel. Don't add direct imports between sibling files — go through_namespaces/ts.js. The local ESLint ruleno-direct-import(inscripts/eslint/rules/no-direct-import.cjs) enforces this.
File naming
- Files are camelCase:
nodeFactory.ts,moduleNameResolver.ts. Directories are camelCase. No hyphens. - Test files in
tests/cases/compilerandtests/cases/conformancealways end in.ts(never.d.ts). - Generated files end with
.generated.ts/.generated.d.tsand are excluded from lint viaeslint.config.mjs.
API surface
- Anything exported from
src/compiler/_namespaces/ts.tsis reachable fromimport * as ts from "typescript". Annotate non-public exports with/** @internal */. ThedtsBundler.mjsscript strips internal exports from the generatedlib/typescript.d.ts. - Don't break the public API without a deprecation cycle. The
src/deprecatedCompat/project keeps shims for previously public APIs.
Coding patterns
Avoid array mutation in expressions
The local rule no-array-mutating-method-expressions (see scripts/eslint/rules/no-array-mutating-method-expressions.cjs) flags arr.sort() and arr.reverse() used as expressions because both mutate. Use [...arr].sort() or one of the project utility functions.
Prefer arrow functions
only-arrow-functions in scripts/eslint/rules/only-arrow-functions.cjs discourages function expressions where an arrow would do, because of this binding pitfalls.
Don't use in for property tests
no-in-operator discourages using in to test for property presence. Use hasProperty(obj, "key") or obj.key !== undefined instead — in walks the prototype chain and behaves badly on Object.create(null) maps used widely in the codebase.
Use Debug.assert* rather than assert
Debug.assertNever, Debug.fail, Debug.assertIsDefined, etc., are tracked by the debug-assert ESLint rule. They give better error messages and integrate with the Debug.isDebugging workflow.
JSDoc comments on public API
jsdoc-format enforces well-formed JSDoc comments. Public exports should have a leading JSDoc comment that downstream consumers (and the compiler's own getQuickInfoAtPosition) can surface.
__String vs string
Symbol names are stored as __String, a branded string that escapes leading underscores. Convert with unescapeLeadingUnderscores before showing to a user. See src/compiler/types.ts.
Path vs string
File paths used as map keys are typed Path (a branded string from src/compiler/types.ts). Always go through toPath() to normalise — Windows backslashes, case-insensitive filesystems, and trailing-slash differences will otherwise cause lookup misses.
Diagnostic messages
User-visible strings live in src/compiler/diagnosticMessages.json (over 8,500 lines). After editing it, run hereby generate-diagnostics to regenerate src/compiler/diagnosticInformationMap.generated.ts. Localised translations live under src/loc/lcl/<locale>/. See the coding guidelines on diagnostic messages for naming conventions.
Performance and allocation
- Many hot paths use
MapLike(a plain object withObject.create(null)-style semantics) rather thanMapbecauseObject.create(null)is faster for short-lived lookups and avoids prototype pitfalls. - The
tracing.tsmodule emits chrome-trace events when--generateTraceis set. Wrap expensive operations intracing?.push(...)/tracing?.pop()blocks so they show up. - Hot loops use plain
forloops, notfor…of, on arrays. Search for "// Use plain for loops because" comments.
Error handling
- The compiler doesn't throw exceptions across the top-level boundary in normal operation. User-visible failures become
Diagnostics. Internal invariants useDebug.fail/Debug.assert. - Unhandled exceptions from a session command bubble up and produce a "TypeScript Server Error" message in editors.
Architectural patterns
Lazy creation
Programs and TypeCheckers are constructed up-front but compute most of their state lazily. The checker's getTypeOfSymbol walks declarations the first time it's called for a symbol; subsequent calls return a cached Type. Most language-service entry points walk only what they need.
ts.factory for AST construction
Don't manually build nodes. Use factory.createXxx (from src/compiler/factory/nodeFactory.ts). Factories preserve invariants like correct parent pointers, proper pos/end ranges, and emit-helper attachments. See systems/factory.
forEachChild and visitNode
Tree walks come in two flavours:
forEachChild(node, cb)— read-only walk used by the binder, checker, and language-service queries.visitNode/visitEachChild— transforming walks used by the transformers.
Don't roll your own walker; both have many corner cases (decorators, type parameters, JSDoc) baked in.
Incrementality and snapshots
Program, BuilderProgram, and LanguageService all support being given a previous instance and reusing as much as possible. Files keep a per-source flag bindDiagnostics/hasJsDocDiagnostics/etc. If you store cached state on a node or symbol, make sure it's invalidated when the source-file is re-parsed.
Testing patterns
See how-to-contribute/testing for runners. A few conventions worth restating here:
- Compiler tests are plain
.tsfiles undertests/cases/compiler/andtests/cases/conformance/. Use// @directive: valuefor compiler options. - Fourslash tests are in
tests/cases/fourslash/. They use////source-line markers and theverifyAPI. - Don't write Mocha unit tests for compiler logic. Add a compiler or fourslash baseline instead.
- New baselines are placed under
tests/baselines/local/. Compare totests/baselines/reference/and accept withhereby baseline-acceptafter manual review.
Cross-cutting
- Line endings are CRLF in source —
.gitattributesenforces this on Windows. Setcore.autocrlf=inputandwhitespace=cr-at-eolin your local config. - Two-space indentation, semicolons required, double-quoted strings — all enforced by
.dprint.jsoncand ESLint.
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.