TypeScript is a typed superset of JavaScript that adds a static type system and compile-time checks while still running as plain JavaScript after compilation. It matters because it lets teams model data shapes, APIs, and module boundaries explicitly, catching many bugs before runtime and improving editor tooling (navigation, refactors, autocomplete). The key mental model is that TypeScript's checking is mostly structural and happens at compile time — your types don't exist at runtime unless you write runtime validation. TypeScript 5.x has added significant capabilities including Stage 3 decorators, explicit resource management (using/await using), the NoInfer<T> utility type, inferred type predicates, and --erasableSyntaxOnly for Node.js type-stripping workflows.
15 tables, 162 concepts. Select a concept node to jump to its table row.
Table 1: Type Annotations & Primitive Types
The starting point: how you attach a type to a value, and the built-in primitives you'll annotate with most often. Often you don't write the type at all, since inference reads it from the initializer, so the real skill is knowing when an explicit annotation earns its keep. Every type here is erased at compile time and none of it survives into the JavaScript you ship. Pay special attention to the three special types at the bottom: any switches checking off, unknown is the safe top type you must narrow first, and never marks code that can't produce a value.
| Concept | Example | Description | |
|---|---|---|---|
let n: number = 42 | • Explicitly assigns a static type to a variable, parameter, or return value • erased at compile time, so it never changes runtime behavior. | ||
const s = "hello" | • Compiler infers a type from an initializer or usage without an annotation • const keeps the literal type "hello", let widens to string. | ||
let name: string = "Ada" | • Primitive text type • Always the lowercase string, not the rarely used built-in String. | ||
let pi: number = 3.14159 | Primitive numeric type (a double-precision float). JS has no int or float, so number covers both. | ||
let ok: boolean = true | • Primitive true/false type • An alias for the union true |• false. | ||
const id: bigint = 123n | • Primitive for arbitrary-size integers (JS bigint)• cannot be mixed with number in arithmetic. | ||
const key: unique symbol = Symbol("k") | • Primitive for unique identifiers • unique symbol supports nominal-like keys, and is allowed only on const and readonly static. | ||
let x: string | undefined | • Represent absence • require explicit handling under strictNullChecks. | ||
let data: any = JSON.parse(text) | • Disables type checking for the value (escape hatch) • spreads to every property you read off it, so avoid in new code. | ||
let data: unknown = JSON.parse(text) | • Safer top type than any• accepts any value, but must be narrowed before property access or calls. | ||
function log(msg: string): void { console.log(msg) } | Indicates a function returns no useful value. Not the same type as undefined. | ||
function fail(msg: string): never { throw new Error(msg) } | Indicates no possible value: unreachable code, always-throws, or an exhausted union. |
Table 2: Object Shapes (Interfaces, Aliases, Properties)
Most real-world typing is describing the shape of objects, and these are the tools for it. Interfaces and type aliases both name a shape: interfaces can be re-opened and merged, while aliases can name any type expression at all, not just objects. On top of either you layer optional and readonly properties, index signatures for open-ended keys, and call or construct signatures for objects that are also callable. The excess property check is the quirk worth remembering: object literals get scrutinized for stray keys in ways stored variables don't.
| Pattern | Example | Description | |
|---|---|---|---|
interface User { id: string; name: string } | • Named object shape • supports extension and declaration merging. | ||
type User = { id: string; name: string } | Gives a name to any type expression: primitives, unions, tuples, objects. | ||
const u: { id: string; name: string } = { id: "1", name: "Ada" } | Inline property names and types without naming the type. | ||
type User = { name: string; email?: string } | Marks a property as possibly absent, adding undefined to its type. | ||
type User = { readonly id: string; name: string } | Prevents assignment after initialization at the type-checking level. | ||
interface Admin extends User { role: "admin" } | Builds larger shapes from smaller ones via extends. | ||
type Headers = { [name: string]: string } | Types objects whose keys aren't known in advance. Declared properties must match the value type. | ||
type Fn = { (x: number): string } | Describes a callable object that may also carry properties. | ||
type Ctor = { new (s: string): Date } | Describes a new-able constructor type. | ||
const u: User = { id: "1", name: "Ada", extra: 1 } | Object literals checked for unknown properties when directly assigned to a type. |
Table 3: Type Composition (Unions, Intersections, Literals, Enums)
This is where types start combining into something more expressive than a single shape. Unions say "one of these", intersections say "all of these at once", and literal types pin a value down to an exact string or number. Together they are the foundation of modeling states precisely. Tuples, as const, and the two flavors of enum round it out, with the notes flagging which constructs emit runtime code (string and numeric enums) versus which vanish at compile time.
| Type | Example | Description | |
|---|---|---|---|
type ID = string | number | Value can be one of multiple types. | ||
type WithId = { id: string } & { createdAt: Date } | Value must satisfy all combined types simultaneously. | ||
const roles = ["admin", "user"] as const | Infers the narrowest literal and readonly types for an expression. | ||
type Role = "admin" | "user" | Restricts values to specific string constants. | ||
type Dice = 1 | 2 | 3 | 4 | 5 | 6 | Restricts values to specific numeric constants. | ||
const pair: [string, number] = ["a", 1] | Fixed-length array with positional element types. | ||
user!.id | Asserts a value is not null/undefined for type checking (erased at runtime). | ||
enum Role { Admin = "admin", User = "user" } | • Members have explicit string values at runtime • readable when logged, but get no reverse mapping. | ||
enum Status { Pending, Done } | • Named constants that emit a runtime JS object • auto-increment from 0, with a reverse mapping back to names. | ||
const enum Dir { Up, Down } | • Inlines enum values at emit time, leaving no runtime object • avoid in public declaration files. |
Table 4: Functions (Signatures, Generics, Overloads)
Typing functions covers everything from a plain parameter list to generics that carry types through a call. Generics and their extends constraints are the heart of reusable, type-safe utilities, while overloads let one implementation present several distinct call signatures. The smaller entries (optional, default, and rest parameters, plus newer touches like const type parameters) handle the everyday details of getting arguments and return values typed cleanly.
| Pattern | Example | Description | |
|---|---|---|---|
type Mapper = (x: number) => string | • Describes a function's parameter and return types as a type alias • The parameter name is required | ||
function first<T>(xs: T[]): T { return xs[0] } | Parameterizes types with <T> for reusable, type-safe logic. | ||
function len<T extends { length: number }>(x: T) { return x.length } | Restricts a type parameter with extends so required members can be accessed. | ||
function parse(x: string): number;function parse(x: number): string;function parse(x: string | number) { return typeof x === "string" ? +x : String(x) } | • Multiple call signatures with one implementation below them • the implementation signature is not callable from outside. | ||
function greet(name?: string) {} | • Parameter may be omitted • typed as possibly undefined inside the function. | ||
function inc(n: number = 1) { return n + 1 } | Supplies a default value when the argument is missing or undefined, so the parameter is not undefined inside. | ||
function sum(...ns: number[]) { return ns.reduce((a,b)=>a+b, 0) } | Collects remaining arguments into a typed array. Must come last and be annotated as an array or tuple. | ||
function identity<const T>(val: T): Tidentity(["a","b"]) // T inferred as readonly ["a","b"], not string[] | Infers the narrowest literal type from a call-site argument (TS 5.0+). Only affects literals written inside the call. | ||
type ApiResult<T = unknown> = { data: T } | Provides a fallback when type arguments are not supplied or inferred. | ||
function f(this: Date) { return this.getTime() } | Fake first parameter to type this (erased in output, never passed as an argument). | ||
type Fn = { (x: string): string; tag: string } | Models callable values that also carry properties. Uses : before the return type, not =>. |
Table 5: Classes (Members, Modifiers, Inheritance)
TypeScript classes are JavaScript classes with a type layer on top: the same fields, methods, and inheritance, plus visibility modifiers, readonly, and implements/abstract for enforcing contracts. Parameter properties save you a step by declaring and assigning a field straight from the constructor signature, and the override keyword (with noImplicitOverride) makes inheritance refactors safe. Watch the erasure boundary: private and readonly are compile-time checks that vanish from the emitted JavaScript, whereas JS # fields stay private at runtime.
| Concept | Example | Description | |
|---|---|---|---|
class Point { x = 0; move(n: number) { this.x += n } } | • Defines fields/methods with type annotations • emits a JS class. | ||
class User { constructor(public readonly id: string) {} } | Shorthand that declares and initializes a member from a constructor parameter. | ||
class A { private secret = 1; protected n = 2; public id = "x" } | • Controls member visibility in type checking • use # for hard JS private fields. | ||
class A { readonly id = "x" } | • Blocks assignment outside the constructor at type-check time • no runtime effect. | ||
class Repo implements Iterable<string> { [Symbol.iterator]() { return [][Symbol.iterator]() } } | Checks a class satisfies an interface at compile time. | ||
class B extends A {} | Subclassing with super calls and overriding. | ||
abstract class Base { abstract run(): void } | • Cannot be instantiated directly • may declare abstract members subclasses must implement. | ||
class B extends A { override toString() { return "B" } } | • Replaces a base member • override keyword enforces the base member exists. | ||
class A { get x() { return this._x } set x(v: number) { this._x = v } } | Declares typed property accessors. | ||
class C { static count = 0; static { C.count = 1 } } | Runs complex static setup code once during class initialization, with access to private members (TS 4.4+). | ||
// tsconfig.json{ "compilerOptions": { "noImplicitOverride": true } } | Requires override keyword on all overriding members for safer refactors. |
Table 6: Narrowing (Type Guards & Control Flow)
Narrowing is how a broad union type collapses to a specific one inside a branch. The compiler watches your control flow and tracks what a value can still be. The everyday guards reuse plain JavaScript checks (typeof, instanceof, in, truthiness, equality), and discriminated unions make this effortless by tagging each variant with a shared literal field. When the built-in checks aren't enough, type predicates and assertion functions let you teach the compiler your own narrowing logic, and the exhaustiveness pattern catches the case you forgot.
| Technique | Example | Description | |
|---|---|---|---|
if (typeof x === "string") x.toUpperCase() | Narrows unions using JS typeof checks. Beware that typeof null is "object". | ||
if (e instanceof Error) console.error(e.message) | Narrows via prototype chain checks. Needs a runtime value, so interfaces don't work. | ||
if ("id" in obj) obj.id | Narrows based on property existence. Optional properties stay in both branches. | ||
if (s) s.trim() | • Narrows based on falsy/truthy JavaScript semantics • Also drops "", 0, and NaN. | ||
if (x === null) return | Narrows unions with === / !== comparisons. Loose != null removes null and undefined at once. | ||
type Shape = { kind: "circle"; r: number } | { kind: "square"; s: number } | • Uses a shared literal field ( kind) to narrow via switch/if checks• A string field won't narrow | ||
function isError(x: unknown): x is Error { return x instanceof Error } | • User-defined guard that narrows a value to a specific type • Unchecked, so the compiler trusts the claim | ||
const isString = (x: unknown) => typeof x === "string" | TS 5.5+ infers x is string when there's no explicit return type and a single return refines the parameter. | ||
function assertNonNull(x: unknown): asserts x { if (x == null) throw new Error() } | Narrows by asserting a condition holds when the function returns normally. | ||
const _exhaustive: never = value | Forces the compiler to prove all union cases are handled. Errors at build time if a case is missed. |
Table 7: Type Operators & Advanced Types
This is type-level programming: operators that compute new types from existing ones rather than just declaring them. keyof, typeof, and indexed access read structure out of other types; mapped types, conditional types, and infer transform and branch on it; template literal types build string types programmatically. It's the most powerful corner of TypeScript and the steepest, but it's what lets a library's types stay perfectly in sync with its data. The satisfies operator is a gentler standout, validating a value against a type without throwing away its narrower inferred shape.
| Operator | Example | Description | |
|---|---|---|---|
const input = document.getElementById("id") as HTMLInputElement | • Overrides TypeScript's inferred type • no runtime effect. Use when you know more than the compiler. | ||
const x = value as unknown as TargetType | • Bypasses the "types don't sufficiently overlap" check via the unknown hop• still no runtime conversion. Use sparingly. | ||
const cfg = { mode: "dev" as const }; type Cfg = typeof cfg | • Extracts a type from a runtime value's shape • Legal only on identifiers and their properties | ||
type Keys = keyof User | • Produces a union of property keys from an object type • a string index signature yields string | number, not literal keys. | ||
type UserId = User["id"] | Looks up a property type from another type via T[K]. The index is itself a type. | ||
const routes = { home: "/" } satisfies Record<string, string> | Checks an expression matches a type without widening the expression's own inferred type. | ||
type RO<T> = { readonly [K in keyof T]: T[K] } | • Creates a new object type by iterating over keys • mapping keyof T keeps readonly/?; strip them with -readonly/-?. | ||
type IsString<T> = T extends string ? true : false | Chooses a type based on an extends assignability test. | ||
type EventName = `on${Capitalize<string>}` | • Builds string types via template literal syntax over string unions • Unions in several slots cross multiply | ||
type Getters<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K] } | • Renames or filters keys while mapping with the as clause• producing never drops the key. | ||
type ToArray<T> = T extends any ? T[] : never | • Distributes over unions when the checked type is a naked type parameter • wrap both sides ( [T] extends [U]) to turn it off. | ||
type Elem<T> = T extends (infer U)[] ? U : T | • Introduces an inferred type variable inside a conditional type • In scope in the true branch only | ||
type ReadRef<out T> = { readonly value: T }type Writer<in T> = { write(x: T): void } | • out = covariant (used in output position only)• in = contravariant (input only); checked, not a hint: a wrong annotation is an error. Improves checker accuracy and speed (TS 4.7+). | ||
type JSON = string | number | boolean | null | JSON[] | { [k: string]: JSON } | • A type alias that references itself • supported since TS 3.7 via deferred resolution at the alias's top level. |
Table 8: Utility Types (Built-in)
These ship with TypeScript and save you from hand-writing the same transformations over and over. Partial, Required, Readonly, Pick, and Omit reshape object types; Record builds dictionaries; Exclude, Extract, and NonNullable filter unions; and ReturnType, Parameters, and Awaited extract pieces out of functions and promises. Two of them bite: Partial and Readonly only go one level deep, and Omit never checks your key against keyof T, so a typo silently removes nothing.
| Type | Example | Description | |
|---|---|---|---|
type Patch = Partial<User> | Makes all properties of T optional (top level only). | ||
type StrictUser = Required<User> | Makes all properties of T required. | ||
type Frozen = Readonly<User> | Makes all properties of T readonly at compile time (top level only, nothing is frozen at runtime). | ||
type UserRef = Pick<User, "id" | "name"> | Selects a subset of properties by key. | ||
type PublicUser = Omit<User, "email"> | • Removes properties by key • Keys aren't checked against keyof T, so a typo silently removes nothing | ||
type ById = Record<string, User> | Maps keys K to value type T. | ||
type NonString = Exclude<string | number, string> | Removes from union T the members assignable to U. | ||
type OnlyString = Extract<string | number, string> | Keeps from union T only the members assignable to U. | ||
type NN = NonNullable<string | null | undefined> | Removes null and undefined from a type. | ||
type R = ReturnType<() => Promise<number>> | Extracts a function type's return type. | ||
type P = Parameters<(x: string, y: number) => void> | Extracts a function type's parameter tuple. | ||
type V = Awaited<Promise<string>> | Recursively unwraps the resolved value type of a promise-like. | ||
class C { constructor(a: string, b: number) {} }type P = ConstructorParameters<typeof C> // [string, number] | Extracts a constructor's parameter types as a tuple. | ||
type I = InstanceType<typeof User> | Extracts the instance type of a constructor function type. | ||
function createStore<T>(init: T, fallback: NoInfer<T>): T { return init ?? fallback } | Blocks type inference from the wrapped position, forcing T to be inferred from other arguments (TS 5.4+). | ||
const mixin: ThisType<{ x: number }> = { getX() { return this.x } } | Specifies the this type for methods in an object literal (requires noImplicitThis). | ||
type U = Uppercase<"hello"> // "HELLO" | Intrinsic string types that transform string literal types to upper/lower case (TS 4.1+). | ||
type C = Capitalize<"world"> // "World" | Intrinsic string types that capitalize or uncapitalize the first character of a string literal type (TS 4.1+). | ||
type T = ThisParameterType<(this: Date) => void> // Date | Extracts the this parameter type from a function type. | ||
type F = OmitThisParameter<(this: Date, x: string) => void> | Removes the this parameter from a function type. |
Table 9: Decorators
Decorators wrap classes and their members to add or alter behavior, using the @-prefixed syntax you have seen in frameworks like Angular and NestJS. TypeScript 5.0 made the standardized TC39 decorators the default: each one receives the decorated value plus a context object and can return a replacement, and 5.2 added a metadata channel via Symbol.metadata. The crucial caveat is that these are not compatible with the older experimentalDecorators form, so a project picks one system for the whole compilation.
| Decorator | Example | Description | |
|---|---|---|---|
function sealed(Base: Function, ctx: ClassDecoratorContext) { Object.seal(Base) } class C {} | • Wraps or replaces a class • receives the class and a context object • return a callable to replace the class, or nothing to keep it. | ||
function log(fn: Function, ctx: ClassMethodDecoratorContext) { return function(this: any, ...args: any[]) { return fn.apply(this, args) } }class C { greet() {} } | • Wraps a method • the replacement function is returned • return nothing and the original method is left alone. | ||
function init<T>(val: T) { return (_: undefined, _ctx: ClassFieldDecoratorContext) => () => val }class C { ("Ada") name!: string } | • Intercepts field initialization • receives undefined, never the field's value• return an initializer function to override the default value. | ||
function bound({ get, set }: ClassAccessorDecoratorTarget<any, string>, ctx: ClassAccessorDecoratorContext) {}class C { accessor name = "Ada" } | • Decorates an accessor keyword field, which auto-generates a getter/setter pair over a private slot• receives that get/set pair and may return { get, set, init }. | ||
function configurable(fn: Function, ctx: ClassGetterDecoratorContext) {}class C { get x() { return 1 } } | • Decorates a getter or setter separately • @configurable on get x() leaves set x() undecorated. | ||
function meta(_: any, ctx: DecoratorContext) { ctx.metadata.info = "x" } class C {}; C[Symbol.metadata] | • TS 5.2+ adds Symbol.metadata to classes• decorators share one metadata object per class, accessible at runtime. | ||
// tsconfig.json{ "compilerOptions": { "experimentalDecorators": true } } | • Enables the pre-TC39 legacy decorator syntax used by Angular, NestJS, and older libraries • methods get (target, propertyKey, descriptor), parameters can be decorated, and emitDecoratorMetadata works• incompatible with the standard decorators, and the flag applies to the whole compilation. |
Table 10: Modules & Imports
How code is split across files and how TypeScript resolves and emits those boundaries. Standard ESM import/export is fully type-checked, and the type-only variants (import type / export type) mark symbols that exist purely for the type system and are erased from the output. The tsconfig cluster here (moduleResolution, module, esModuleInterop, paths) reconciles TypeScript with the realities of Node.js and bundlers, and is a frequent source of confusing import errors. Watch the emit boundary: paths and allowSyntheticDefaultImports change type checking only, so code that compiles cleanly can still fail at runtime.
| Pattern | Example | Description | |
|---|---|---|---|
import { readFile } from "node:fs/promises" | • Standard ECMAScript module syntax, fully type-checked by TS • Any file with a top-level import or export is a module• without one it is a script in the global scope | ||
import type { User } from "./types" | Imports a symbol only for the type system, guaranteed to be erased from the emitted JavaScript. | ||
export type { User } from "./types" | Re-exports types without a runtime export. | ||
import data from "./data.json" with { type: "json" } | • Instructs the runtime how to load a module (e.g., JSON) • Contents are not checked by TS; they pass through to the host. Supersedes the deprecated assert syntax; supported from TS 5.3. | ||
// tsconfig.json{ "compilerOptions": { "verbatimModuleSyntax": true } } | Leaves any import/export without a type modifier in the output verbatim and drops anything marked type, so what you write is what you get. | ||
import pkg from "cjs-only" | • Covers how TS models default/namespace imports across ESM and CommonJS boundaries • Node gives every CJS module a synthetic default export of its whole module.exports. | ||
// tsconfig.json{ "compilerOptions": { "esModuleInterop": true } } | • Adjusts emit and checking for default/namespace imports to match Node.js/bundler behavior • Adds __importDefault/__importStar helpers and implies allowSyntheticDefaultImports. | ||
// tsconfig.json{ "compilerOptions": { "allowSyntheticDefaultImports": true } } | Allows default imports from modules without a default export (type-checking convenience only). | ||
// tsconfig.json{ "compilerOptions": { "moduleResolution": "bundler" } } | • Selects how imports are resolved: node16, nodenext, or bundler for modern tooling. bundler never requires file extensions on relative imports• the Node modes do | ||
// tsconfig.json{ "compilerOptions": { "module": "nodenext" } } | Selects the emitted module format and influences resolution behavior. | ||
// tsconfig.json{ "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["src/*"] } } } | Adds path mapping for module specifiers during type checking only. TS does not rewrite the specifier in emitted JS, so an alias needs a bundler or tsc-alias to work at runtime. paths values resolve relative to baseUrl when it is set. |
Table 11: JSX / TSX
If you write React or any JSX-based UI, these are the knobs that make it type-check. The .tsx extension turns on JSX parsing, the jsx compiler option decides whether that syntax is transformed or preserved, and jsxImportSource points the automatic runtime at the right factory, so you can target Preact or another library instead of React. JSX.IntrinsicElements is where the set of valid built-in tags and their props lives.
| Option | Example | Description | |
|---|---|---|---|
// Component.tsxexport function Button() { return <button /> } | • Enables JSX parsing in a source file • The jsx compiler option must be set too | ||
// tsconfig.json{ "compilerOptions": { "jsx": "react-jsx" } } | Controls how JSX is transformed or preserved by the compiler. | ||
// tsconfig.json{ "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "preact" } } | Sets the module for importing jsx/jsxs factories for the automatic runtime. | ||
declare namespace JSX { interface IntrinsicElements { div: any } } | Declares valid JSX tags and their prop types for type checking. |
Table 12: TSConfig Core (Type Checking, Emit, Projects)
The compiler options you'll actually set in tsconfig.json, grouped around three jobs: how strictly to check, what to emit, and how to structure multi-package builds. strict is the single most consequential choice, but read it as a named family of flags rather than a master switch: noUncheckedIndexedAccess and exactOptionalPropertyTypes sit outside it and must be opted into by hand. The other trap worth internalising early is that target downlevels syntax and never adds polyfills, so a green build can still crash on an old engine.
| Option | Example | Description | |
|---|---|---|---|
// tsconfig.json{ "compilerOptions": { "strict": true } } | Enables the whole strict family at once ( strictNullChecks, noImplicitAny, useUnknownInCatchVariables, and others). The recommended baseline, but not every check: noUncheckedIndexedAccess and exactOptionalPropertyTypes stay opt-in. | ||
// tsconfig.json{ "compilerOptions": { "strictNullChecks": true } } | Treats null/undefined as distinct types requiring explicit handling. | ||
// tsconfig.json{ "compilerOptions": { "noImplicitAny": true } } | • Errors when the compiler would infer any due to missing type information• Explicit any is untouched | ||
// tsconfig.json{ "compilerOptions": { "target": "ES2022" } } | • Sets the emitted JS language level and downlevels syntax only. Never adds polyfills for runtime APIs • Also changes the default lib. | ||
// tsconfig.json{ "compilerOptions": { "lib": ["ES2022", "DOM"] } } | Selects the built-in declaration libraries the checker loads (e.g., DOM, ES2022). Type definitions only: nothing from lib reaches your output. | ||
// tsconfig.json{ "compilerOptions": { "skipLibCheck": true } } | • Skips type checking of .d.ts files themselves, speeding up builds• their types still apply to your code. | ||
// tsconfig.json{ "compilerOptions": { "resolveJsonModule": true } } | Allows importing .json files with full type inference from their content. | ||
// tsconfig.json{ "compilerOptions": { "isolatedModules": true } } | Errors on patterns that require cross-file type information, making single-file transpile tools safe (e.g., esbuild, SWC). Changes no output. | ||
// tsconfig.json{ "compilerOptions": { "declaration": true } } | Emits a .d.ts per input file describing the public types of each module. | ||
// tsconfig.json{ "compilerOptions": { "noEmit": true } } | • Runs the full type check without writing any output files • The standard CI check when another tool builds | ||
// tsconfig.json{ "compilerOptions": { "incremental": true } } | Saves project-graph info to a .tsbuildinfo file to speed up subsequent compilations. | ||
// tsconfig.json{ "compilerOptions": { "composite": true } } | • Marks a project reference-able by other projects • Implies declaration, defaults incremental to true, and defaults rootDir to the config's folder | ||
// tsconfig.json{ "references": [{ "path": "../core" }] } | Declares dependencies between TS projects. Only tsc -b builds them: plain tsc -p reads their last-built .d.ts. | ||
// tsconfig.json{ "compilerOptions": { "noUncheckedIndexedAccess": true } } | Adds undefined to every indexed read (arr[i], obj[key]). Deliberately not in strict, since it fires even in bounds-checked loops. | ||
// tsconfig.json{ "compilerOptions": { "exactOptionalPropertyTypes": true } } | • Makes optional properties more exact: assigning undefined is rejected, distinct from the property being absent• Not part of strict. | ||
// tsconfig.json{ "compilerOptions": { "useUnknownInCatchVariables": true } } | Types catch (e) as unknown instead of any, forcing you to narrow before use. Included in strict. | ||
// tsconfig.json{ "compilerOptions": { "isolatedDeclarations": true } } | • Requires exports to be annotated enough that other tools can generate .d.ts files without a type checker• Needs declaration or composite (TS 5.5+). | ||
// tsconfig.json{ "compilerOptions": { "erasableSyntaxOnly": true } } | Prohibits TS syntax with runtime semantics (enums, namespaces with runtime code, parameter properties, import =) so Node's type-stripping can just delete types (TS 5.8+). |
Table 13: tsc CLI Workflows
The handful of tsc invocations that cover almost everything you'll do from the command line. tsc --noEmit is the one CI pipelines lean on, since it type-checks without writing a single output file, while --watch keeps recompiling as you edit and tsc --init scaffolds a fresh config. The -b build mode is the one to know once a codebase grows into multiple referenced projects, since it rebuilds them in dependency order. Watch the one big trap: naming any file on the command line makes tsc ignore tsconfig.json completely.
| Command | Example | Description | |
|---|---|---|---|
tsc | • Compiles using the nearest tsconfig.json, searching the current directory then up the parent chain• Naming files instead makes tsc ignore that config | ||
tsc --noEmit | Runs the full type check while writing no JavaScript, declarations, or source maps. | ||
tsc --watch | • Recompiles on file changes continuously • It rebuilds only, it never re-runs your program | ||
tsc --init | Creates a tsconfig.json scaffold with annotated options. | ||
tsc -p tsconfig.build.json | • Compiles the project at an explicit config path, either a config file or a folder • Pass no input files alongside it | ||
tsc -b | Builds a referenced project graph in dependency order, skipping projects already up to date. | ||
tsc -b --watch | Incrementally rebuilds a project reference graph on changes. | ||
tsc --version | Prints the version of the compiler that is running. |
Table 14: Declaration Files & Augmentation
Declaration files (.d.ts) carry types without any runtime code — they're how plain JavaScript packages describe their shapes and how you fill gaps in third-party types. Augmentation is the powerful part: declare global and module augmentation let you add members to existing types (a userId on Express's Request, say) without forking the original package, and declaration merging is the underlying mechanism that makes it work.
| Pattern | Example | Description | |
|---|---|---|---|
// index.d.tsexport interface User { id: string } | Provides type information for JS at compile time with no runtime code. | ||
// globals.d.tsdeclare const VERSION: string | Declares globals accessible without imports (ambient global library style). | ||
// index.d.tsdeclare module "pkg" { export function f(): void } | Declares the shape of an importable module for untyped JS packages. | ||
declare global { interface Window { appVersion: string } } | Augments global types from within a module (requires at least one import or export). | ||
declare module "express-serve-static-core" { interface Request { userId?: string } } | Adds members to an existing module's declarations without forking the types package. | ||
interface Box { size: number }interface Box { color: string } | Multiple declarations with the same name are merged into one combined type. | ||
/// <reference types="node" /> | • Declares a type package dependency for the file • in .ts files, set types in tsconfig.json instead. | ||
/// <reference lib="dom" /> | Explicitly includes a built-in lib (e.g., dom) in the compilation without changing tsconfig.json. |
Table 15: Explicit Resource Management
Deterministic cleanup, built into the language: try/finally you no longer have to write by hand. Declare a resource with using (or await using for async cleanup) and its [Symbol.dispose] method runs automatically when the enclosing block exits, so connections and file handles close on an early return or a thrown error too. Resources are released in reverse order of declaration, the Disposable interface defines what makes a class eligible, and DisposableStack groups several resources to tear down together.
| Feature | Example | Description | |
|---|---|---|---|
using conn = getConnection() | • Calls conn[Symbol.dispose]() when the enclosing block exits, including on early return, break, and throw• null/undefined are a no-op (TS 5.2+, ECMAScript Stage 4). | ||
await using conn = await openConnection() | • Calls and awaits conn[Symbol.asyncDispose]() when the enclosing block exits• falls back to [Symbol.dispose] if there is no async disposer• the await marks disposal only, so acquisition needs its own await. | ||
class Conn { [Symbol.dispose]() { this.close() } } | • The synchronous cleanup method called by using• implement to make a class disposable. | ||
class Conn { async [Symbol.asyncDispose]() { await this.close() } } | The asynchronous cleanup method called by await using. | ||
function use(r: Disposable) { using _ = r; } | • Interface requiring [Symbol.dispose](): void• available with lib set to "esnext" or "esnext.disposable". | ||
async function use(r: AsyncDisposable) { await using _ = r; } | Interface requiring [Symbol.asyncDispose](), returning a promise. | ||
using stack = new DisposableStack()stack.use(resource); stack.defer(() => cleanup()) | • Aggregates multiple resources for disposal in LIFO order • use() takes a disposable, defer() a callback, adopt() a value plus its disposer• move() transfers ownership• AsyncDisposableStack for async variants. | ||
err.error / err.suppressed | • Wraps both errors when disposal throws while another error is already in flight • error is the later throw, suppressed the earlier one. |