Open-Source Wikis

/

TypeScript

/

How to contribute

/

Patterns and conventions

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.json that extends src/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 rule no-direct-import (in scripts/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/compiler and tests/cases/conformance always end in .ts (never .d.ts).
  • Generated files end with .generated.ts / .generated.d.ts and are excluded from lint via eslint.config.mjs.

API surface

  • Anything exported from src/compiler/_namespaces/ts.ts is reachable from import * as ts from "typescript". Annotate non-public exports with /** @internal */. The dtsBundler.mjs script strips internal exports from the generated lib/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 with Object.create(null)-style semantics) rather than Map because Object.create(null) is faster for short-lived lookups and avoids prototype pitfalls.
  • The tracing.ts module emits chrome-trace events when --generateTrace is set. Wrap expensive operations in tracing?.push(...)/tracing?.pop() blocks so they show up.
  • Hot loops use plain for loops, not for…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 use Debug.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 .ts files under tests/cases/compiler/ and tests/cases/conformance/. Use // @directive: value for compiler options.
  • Fourslash tests are in tests/cases/fourslash/. They use //// source-line markers and the verify API.
  • 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 to tests/baselines/reference/ and accept with hereby baseline-accept after manual review.

Cross-cutting

  • Line endings are CRLF in source — .gitattributes enforces this on Windows. Set core.autocrlf=input and whitespace=cr-at-eol in your local config.
  • Two-space indentation, semicolons required, double-quoted strings — all enforced by .dprint.jsonc and ESLint.

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

Patterns and conventions – TypeScript wiki | Factory