TypeScript — Reference

Source: https://www.typescriptlang.org/docs/

TypeScript

  • Created: 2012 by Microsoft (lead architect: Anders Hejlsberg, also of C# and Turbo Pascal)
  • Latest stable: 6.0.3 (2026-04-16); TypeScript 7.0 Beta announced 2026-04-21 — compiler rewritten in Go, ~10x faster
  • Paradigms: multi-paradigm — OO, functional, structural, generic; superset of JavaScript
  • Typing: static, structural (duck-typed at the type level), gradual (opt-in via --strict), with sound-ish but deliberately unsound parts (variance on arrays, function parameter bivariance in some configs)
  • Memory: N/A — TypeScript erases at compile time; runtime is whatever JS engine runs the emitted code
  • Compilation: transpiled to JS by tsc (or swc/esbuild/oxc/Babel with type-stripping). Type-checking is separable from emit. TS 7.0 ships a Go-based native compiler with the same semantics.
  • Primary domains: large-scale frontend apps, full-stack TS (Next.js, NestJS), CLI tools, libraries, infrastructure-as-code (CDK, Pulumi), backends, anywhere JS runs and the team wants a type system
  • Official docs: https://www.typescriptlang.org/docs/

At a glance

  • Owner: Microsoft. Spec: no formal ECMA spec — the implementation is the spec, with intent documented in the Handbook + lib.d.ts ambient types.
  • Compatibility: TS tracks ECMAScript closely; target controls JS output (ES2015..ESNext). lib controls which built-ins the type system knows about.
  • Release cadence: ~3-month minor releases; @beta/@rc tags on npm. TS 6.0 was the last JS-implemented compiler; 7.0 is the Go rewrite (project codename “Corsa”).
  • No runtime. TS adds type syntax + a few runtime helpers (enum, namespace, decorators if you opt in). Strip the types and you have valid JS.

Getting started

Install:

npm i -D typescript          # local (preferred)
npm i -g typescript          # global tsc
# or
pnpm add -D typescript
bun add -d typescript

For TS 7 native compiler (when GA): a single binary, no Node required.

Hello world:

// hello.ts
const greet = (name: string): string => `Hello, ${name}!`;
console.log(greet("world"));

Compile + run: tsc hello.ts && node hello.js. Or run directly: tsx hello.ts, bun hello.ts, deno run hello.ts, or node --experimental-strip-types hello.ts (Node 22.6+).

Project layout:

myapp/
  package.json
  tsconfig.json
  src/
    index.ts
  test/
    index.test.ts
  dist/                # tsc output (or use a bundler)

tsconfig.json minimum-viable:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "outDir": "dist"
  },
  "include": ["src/**/*"]
}

Build / runner tools:

  • tsc — official, slow but canonical for type-checking. TS 7 native compiler is much faster.
  • tsx, ts-node — run .ts directly in Node.
  • swc, esbuild, oxc — strip types only (no checking), 10-100x faster than tsc emit.
  • node --experimental-strip-types (22.6+) and Bun/Deno run .ts natively.

Playground: https://www.typescriptlang.org/play

Basics

Primitive types: number, bigint, string, boolean, symbol, undefined, null, void, never, unknown, any, object. Literal types: "red", 42, true. Tuples: [string, number].

Variables: same as JS — const, let. Type annotations are optional and inferred from initializers / context.

const port: number = 8080;
let user = { name: "alice" };  // inferred: { name: string }

Control flow: identical to JS. Type narrowing by typeof, instanceof, in, equality, custom predicates (x is Foo), discriminated unions.

Functions:

function fetchAt<T>(url: string, opts: { timeout?: number } = {}): Promise<T> { ... }
const log = (msg: string, level: "info" | "warn" = "info") => console[level](msg);

Overloads via multiple signatures sharing one implementation:

function pad(n: number): string;
function pad(n: number, ch: string): string;
function pad(n: number, ch = "0") { return String(n).padStart(2, ch); }

Strings & collections: all JS — plus literal-typed arrays / tuples and Readonly<T>.

Intermediate

Type system depth:

  • Generics: function id<T>(x: T): T, with constraints <T extends keyof O>, defaults <T = string>.
  • Inference: flow-based, contextual; infer extracts within conditional types.
  • Variance: functions are bivariant on parameters with strictFunctionTypes: false, contravariant with it on. Arrays are covariant (unsound, like Java arrays).
  • Union & intersection: A | B, A & B. Discriminated unions are the workhorse pattern.
  • Utility types: Partial, Required, Readonly, Pick, Omit, Record, Exclude, Extract, NonNullable, Parameters, ReturnType, Awaited, Uppercase/Lowercase/Capitalize/Uncapitalize.

Modules: ESM only in modern code. import { x } from "./mod.js" (note: .js extension even in source, with NodeNext resolution). import type { X } for type-only imports (erased). Path aliases via compilerOptions.paths + bundler/resolver support.

Errors: same as JS (exceptions). try/catch (e: unknown) then narrow with instanceof Error. Result-type pattern is popular but not built-in:

type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };

neverthrow, effect, ts-results are common libs.

Concurrency: all JS — Promises, async/await, AsyncIterators. Promise.all / allSettled / any. Type system tracks Awaited<T> correctly.

File I/O / networking: runtime-dependent. Use @types/node for Node, lib.dom.d.ts for browser, @types/bun / @types/deno for those.

Stdlib highlights (type side): lib.es*.d.ts files in the TS install describe the JS standard library. lib.dom.d.ts describes the browser. @types/* packages from DefinitelyTyped (https://github.com/DefinitelyTyped/DefinitelyTyped) supply third-party JS lib types.

Advanced

Memory model & GC: delegated to runtime (V8 / JSC / SpiderMonkey). TS adds zero overhead — types are erased.

Concurrency: delegated to runtime. TS does help with type-safe Worker messaging — type the MessagePort payload via discriminated unions.

FFI / interop: types for node:*, WebAssembly (WebAssembly.Module), napi-rs, bun:ffi, Deno.dlopen. WASM bindings via wasm-bindgen produce .d.ts.

Reflection: TS has none at runtime by design. typeof x returns JS primitive name. Tools that fake reflection use:

  • Decorators with --emitDecoratorMetadata + reflect-metadata (legacy, used by NestJS/TypeORM).
  • TS-Reflect, tsyringe, class-transformer.
  • Schema-first runtime validators: Zod, Valibot, ArkType, TypeBox, io-ts — define once, derive type via z.infer.
  • Compile-time codegen: ts-morph, ts-patch, custom transformers.

Performance tuning (compiler):

  • tsc --extendedDiagnostics — show parse/check/emit times. Look at “Check time” (the type-checker), “Bind time”, “Parse time”, and instantiation counts.
  • tsc --generateTrace traceDir then open in Chrome chrome://tracing — visualizes per-file checker time, finds the offending generic / conditional type.
  • tsc --diagnostics --listFiles to see which files are being included; common cause of slow checks is accidentally pulling all of node_modules via wildcard include.
  • Incremental builds: tsc -b --incremental.tsbuildinfo caches the type-checker state, second runs are typically 10-50x faster.
  • Project references for monorepos — references: [{ path: "../shared" }] lets tsc -b rebuild only changed projects in topo order, parallelizing across cores.
  • skipLibCheck: true for big node_modules — skips checking .d.ts files (your code still gets fully checked). Universal recommendation; 30-70% faster.
  • moduleResolution: "bundler" (5.0+) — for Vite/esbuild/Bun consumers; skips Node’s awkward .js extension dance.
  • tsperf (Microsoft) and @typescript/analyze-trace — postprocess --generateTrace output, surface “hot types”.
  • TS 7 (Go compiler) — typically 10x faster type-check; same diagnostics, same semantics. Microsoft’s benchmark on the VS Code repo: 77s → 7.5s full check. Editor mode (tsserver) gets sub-second response on the largest monorepos.
  • isolatedDeclarations (5.5+) — requires explicit return-type annotations on exports, lets swc/oxc/esbuild emit .d.ts files without invoking tsc. Critical for fast library builds.

God mode

Conditional types + infer:

type ReturnType<T> = T extends (...a: any) => infer R ? R : never;
type ElementOf<T> = T extends readonly (infer U)[] ? U : never;

Mapped types:

type Mutable<T> = { -readonly [K in keyof T]: T[K] };
type Stringify<T> = { [K in keyof T]: string };

Template literal types:

type Route = `/${"users" | "posts"}/${string}`;
type Camel<S> = S extends `${infer A}_${infer B}` ? `${A}${Camel<Capitalize<B>>}` : S;

Variadic tuples:

type Tail<T extends any[]> = T extends [any, ...infer R] ? R : [];
function pipe<T, A>(x: T, f: (x: T) => A): A;
function pipe<T, A, B>(x: T, f: (x: T) => A, g: (x: A) => B): B;

Branded / nominal types:

type UserId = string & { readonly __brand: unique symbol };
const make = (s: string) => s as UserId;

Declaration merging:

interface Window { myAnalytics: { track(e: string): void } }
declare global { namespace NodeJS { interface ProcessEnv { API_KEY: string } } }

satisfies operator (4.9+): validate without widening.

const config = { host: "localhost", port: 8080 } satisfies Record<string, string | number>;
config.port.toFixed(2);  // still typed as number

const type parameters (5.0+):

function freeze<const T>(x: T): T { return x; }
const x = freeze(["a", "b"]);   // type: readonly ["a", "b"]

Custom transformer plugins: ts-patch (since TS rejected first-party plugin support in tsc emit). Manipulate the AST during compile — used by tsyringe, @nestjs/cli SWC plugin, ts-auto-mock.

Compiler API: import * as ts from "typescript". Build linters, codemods, custom checkers. ts-morph is the friendlier wrapper.

Type-level computation tricks: Peano-style integer math, type-level parsers, JSON parsers, SQL parsers — see type-challenges and Anders Hejlsberg-flavored insanity. Watch out for Type instantiation is excessively deep and possibly infinite — TS limits recursion depth (typically ~50-100 for tail-recursive conditional types since 4.5).

Runtime stripping options:

  • --isolatedModules is required when using non-tsc transpilers; flags constructs they can’t handle alone.
  • --verbatimModuleSyntax (5.0+) — emit imports exactly as written, removing the import type / import ambiguity.
  • TC39 Type Annotations as Comments proposal (Stage 1) — would let JS engines parse-and-ignore TS-style annotations natively, eliminating the strip step entirely. Champions include Microsoft, Bloomberg, Igalia. Years away.
  • Native TS support landed in Node 22.6+ (--experimental-strip-types for syntax-only, --experimental-transform-types for enums/decorators/namespaces in 23+), Bun (full TS native since 0.1), Deno (since 1.0).

Type-level computation cookbook

TypeScript’s type system is Turing-complete (proven via Tic-Tac-Toe, SQL parsers, regex engines). Practical patterns:

// Length of a tuple
type Length<T extends readonly any[]> = T["length"];
 
// Reverse a tuple
type Reverse<T extends any[]> = T extends [infer H, ...infer R] ? [...Reverse<R>, H] : [];
 
// Concat strings
type Join<T extends string[], D extends string = ""> =
  T extends [infer F extends string, ...infer R extends string[]]
    ? R extends [] ? F : `${F}${D}${Join<R, D>}`
    : "";
 
// Parse "user:id" route params
type Params<S extends string> =
  S extends `${infer _Start}:${infer Param}/${infer Rest}`
    ? { [K in Param | keyof Params<Rest>]: string }
    : S extends `${infer _Start}:${infer Param}`
      ? { [K in Param]: string }
      : {};
 
type T = Params<"/users/:id/posts/:postId">; // { id: string; postId: string }

Recursion depth limits: ~50 calls without infer extends, ~1000 with tail-recursive conditional types (TS 4.5+). Hit the limit and you get Type instantiation is excessively deep and possibly infinite. Convert to accumulator-style tail recursion or break into smaller helpers.

Variance annotations (4.7+):

interface Producer<out T> { make(): T; }     // covariant
interface Consumer<in T> { use(x: T): void; } // contravariant
interface Transform<in out T> { both(x: T): T; } // invariant

Use these to make subtyping intent explicit and catch bivariance bugs.

NoInfer<T> (5.4+): block inference from one position so another position drives it.

function clamp<T extends number>(value: T, min: NoInfer<T>, max: NoInfer<T>): T;
clamp(5, 0, 10);  // T inferred from `value` only, won't widen to `5 | 0 | 10`

using declarations (5.2+, ES2026 disposable resources) — type-checked Symbol.dispose / Symbol.asyncDispose for deterministic cleanup:

async function readData() {
  await using db = await connect();   // calls db[Symbol.asyncDispose]() at scope exit
  return db.query("SELECT 1");
}

Idioms & style

  • Naming: camelCase for vars/functions, PascalCase for classes/types/interfaces/enums. No I-prefix on interfaces (Microsoft’s own guidance contradicts old C# habit).
  • Formatters: Prettier, Biome, dprint — all support TS.
  • Linters: @typescript-eslint with eslint, Biome, oxlint. Recommended ruleset: @typescript-eslint/recommended-type-checked.
  • Style guides: Microsoft’s TypeScript Coding Guidelines (internal, public excerpts), Google TypeScript Style Guide (https://google.github.io/styleguide/tsguide.html), tsconfig/bases.
  • Idiomatic patterns:
    • interface for object/class shapes you might extend, type for unions, intersections, mapped/conditional types. Both work for most cases.
    • Discriminated unions over class hierarchies for state machines.
    • unknown not any at I/O boundaries; narrow with type guards or Zod parse.
    • as const to lock literal types in fixtures and config.
    • satisfies for “this matches a shape but keep my narrower type.”
    • Avoid as (type assertions) — almost always means a missing guard or a bad type.
    • Prefer readonly arrays and Readonly<T> for inputs.
  • Reviewer tells: stray any, missing await, // @ts-ignore without explanation (use @ts-expect-error so it errors when fixed), Object (use object or a shape), Function (use a specific signature), enum (prefer literal unions or as const objects).

Ecosystem

DomainTools
FrontendReact, Vue, Svelte, Solid, Angular (TS-first), Lit, Qwik
Meta-frameworksNext.js, Nuxt, SvelteKit, Remix, Astro, Qwik City
BackendNestJS, Fastify (TS-friendly), Hono, ElysiaJS, AdonisJS, tRPC, Encore
Schema / validationZod, Valibot, ArkType, TypeBox, Effect/Schema, io-ts, Yup
ORMPrisma, Drizzle, Kysely, MikroORM, TypeORM
BuildVite, esbuild, swc, tsc, Turbopack, Rolldown, oxc
TestVitest (TS-first), Jest, node:test, Playwright, Mocha
MonorepoNx, Turborepo, Moon, pnpm workspaces, Bun workspaces
StateTanStack Query, Zustand, Jotai, Redux Toolkit, MobX
LLM SDKsOpenAI, Anthropic, Vercel AI SDK, LangChain.js, Mastra
Notable usersMicrosoft (origin, VS Code), Slack, Airbnb, Stripe, Vercel, Bloomberg, Discord, Shopify (Hydrogen)

Schema + validation deep dive

The runtime-vs-type bridge is the single biggest practical TS architecture choice. Library shootout (2024-26):

LibBundleSpeedStyleNotes
Zod 4~12 KBmediumbuilderMost popular; z.infer<T>; v4 dropped IE11 baggage, 4x faster than v3
Valibot~3 KB tree-shakenfastfunctionalDesigned for bundle size; chainable + pipe; v1.0 (2025)
ArkType~25 KBfasteststring DSLtype({ name: "string", age: "number"}); compiles to optimized validators
TypeBox~10 KBfastJSON SchemaOutput is real JSON Schema — direct OpenAPI export
Effect/Schema(with effect-ts)fastfunctionalEncoders + decoders, error tracking via Effect
io-tssmallslowfp-ts styleLegacy; superseded by Effect/Schema
YupmediumslowbuilderReact-Hook-Form default; older
JoilargemediumbuilderServer-side; weak TS inference

End-to-end type safety patterns:

  • tRPC — share types directly between client and server; no codegen, no schema.
  • Hono RPC / Elysia Eden — type the route, get Treaty<typeof app> on the client.
  • OpenAPI codegenopenapi-typescript, orval, kubb generate types from OpenAPI spec.
  • GraphQLgraphql-codegen + gql.tada + gqty generate types from schema or queries.
  • Drizzle / Prisma / Kysely ORMs — the schema is the type; rows return fully typed.

Modern build / runtime matrix

ToolSpeed vs tscTS handlingWhen
tsc1x baselinefull check + emitType-checking gate, lib .d.ts emit
tsc (TS 7 Go)10xfull check + emitReplacement for tsc when GA
swc20-30xstrip onlyProduction builds via Next.js, Parcel
esbuild50-100xstrip onlyVite dev, library builds with isolatedDeclarations
oxc50-100xstrip onlyRolldown internals; oxlint for linting
bun30-100xstrip + runDev/test runner with native execution
denon/a (V8 + swc)strip + rundeno run x.ts, JSR ecosystem
node 23+n/a (amaro/swc)strip + run--experimental-strip-types for .ts files

The standard 2026 architecture: tsc / TS 7 as a type-check-only CI gate (tsc --noEmit) + esbuild / swc / oxc for emit. isolatedDeclarations lets non-tsc tools emit .d.ts for libraries.

Gotchas

  • Type erasure: runtime has no clue about your types. instanceof only works on classes; for shapes you need a runtime validator (Zod et al.).
  • any is contagious — once it touches a value, all derived values become any. Prefer unknown.
  • Bivariant function params — without strictFunctionTypes, callbacks can be substituted unsoundly.
  • Excess property checks only fire on object literals, not on variables. fn({ extra: 1 }) errors but fn(obj) may not.
  • enum — emits runtime objects, has reverse-mappings, doesn’t tree-shake well. Prefer as const objects + literal unions.
  • never propagatesPromise<never> is a Promise that never resolves; check your generics.
  • Module resolution surprisesnode vs node10 vs node16 vs bundler vs nodenext — pick deliberately.
  • .js extension in TS source when emitting ESM — required by Node ESM resolver, looks weird at first.
  • paths aliases without a runtime resolver (tsx, ts-node, or bundler) will fail at runtime.
  • useDefineForClassFields flips field initialization semantics between TC39 and old TS behavior; common with Angular/MobX surprises.
  • Newcomers from C#/Java: TS is structural, not nominal. class Foo {} and class Bar {} with the same shape are assignable. Use brands.
  • Newcomers from JS: types disappear at runtime. Validate user input with Zod or similar before trusting types.
  • readonly is shallowreadonly T[] lets you mutate T’s fields, just not reassign the array. Use DeepReadonly<T> from type-fest or recursive Readonly<T>.
  • Record<string, T> lies — accessing obj["nonexistent"] returns T, not T | undefined. Enable noUncheckedIndexedAccess: true.
  • Array.prototype.includes is overly strict["a"].includes(x) requires x: "a", not x: string. Common workaround: assertion or as readonly string[].
  • Decorators flavor mismatch — Stage 3 decorators (TS 5.0+, no experimentalDecorators) and legacy decorators (Angular, NestJS, TypeORM) are incompatible. Pick one per project.
  • tsconfig.json extends resolution — extends are resolved relative to the extending file path; paths in the extended config don’t rebase. Use ${configDir} (5.5+) or absolute-ish patterns.
  • Module resolution node vs node16 vs nodenext vs bundler — different rules for .js/.ts extensions, exports field handling, conditional exports. nodenext is the strictest, mirrors actual Node ESM behavior.
  • Type inference contamination — type errors in one file can cascade to others via inference. Adding explicit return types to public APIs isolates failures and speeds up checking.

TypeScript version timeline (5.0 → 7.0)

VersionReleasedHeadline
5.0Mar 2023Stage 3 decorators, const type params, extends on type params, —moduleResolution bundler, all-enum constants
5.1Jun 2023Implicit returns of undefined, easier JSX element types, namespaced JSX attributes
5.2Aug 2023using and await using declarations, decorator metadata, named/anonymous tuples
5.3Nov 2023Import attributes (with { type: "json" }), narrowing on instanceof with Symbol.hasInstance
5.4Mar 2024NoInfer<T>, preserved narrowings in closures, Object.groupBy/Map.groupBy types
5.5Jun 2024Inferred type predicates, control-flow narrowed indexed accesses, ${configDir} in tsconfig, isolatedDeclarations
5.6Sep 2024Disallowed nullish/truthy checks always-true diagnostics, ArrayBuffers types, region-prioritized diagnostics
5.7Nov 2024Never-initialized variables errors, path rewriting for relative imports, --target esnext
5.8Mar 2025Granular --erasableSyntaxOnly, search-by-type imports
6.02025Last JS-implemented compiler line
7.02026 (beta Apr 2026)Native compiler in Go — ~10x faster type-check, same diagnostics

The TS 7 rewrite (“Project Corsa”) was led by Anders Hejlsberg and the TS team. Microsoft chose Go over Rust for AST-walker friendliness, fast iteration, and existing Hejlsberg expertise. Maintains the existing tsc JS implementation as the reference until 7 is GA.

Real code: end-to-end type-safety patterns

tRPC: client and server share types

// server/router.ts
import { initTRPC } from "@trpc/server";
import { z } from "zod";
 
const t = initTRPC.create();
 
export const appRouter = t.router({
    getUser: t.procedure
        .input(z.object({ id: z.number() }))
        .query(({ input }) => ({ id: input.id, name: "Alice" })),
    createUser: t.procedure
        .input(z.object({ name: z.string().min(1), email: z.string().email() }))
        .mutation(({ input }) => ({ id: 42, ...input })),
});
 
export type AppRouter = typeof appRouter;
 
// client.ts
import { createTRPCProxyClient, httpBatchLink } from "@trpc/client";
import type { AppRouter } from "./server/router";
 
const client = createTRPCProxyClient<AppRouter>({
    links: [httpBatchLink({ url: "http://localhost:3000/trpc" })],
});
 
const user = await client.getUser.query({ id: 1 });   // typed
//    ^? { id: number; name: string }
 
await client.createUser.mutate({ name: "Bob", email: "bob@x.com" });   // typed input

Zod schema + inferred type + branded ID

import { z } from "zod";
 
const UserIdSchema = z.string().uuid().brand<"UserId">();
type UserId = z.infer<typeof UserIdSchema>;
 
const UserSchema = z.object({
    id: UserIdSchema,
    email: z.string().email(),
    age: z.number().int().min(0).max(150),
    role: z.enum(["admin", "user", "guest"]),
    createdAt: z.coerce.date(),
});
 
type User = z.infer<typeof UserSchema>;
// Same shape as the schema; ID can't be confused with other UUIDs
 
const result = UserSchema.safeParse(JSON.parse(input));
if (!result.success) console.error(result.error.flatten());
else useUser(result.data);

Drizzle ORM: schema → typed queries

import { drizzle } from "drizzle-orm/node-postgres";
import { pgTable, serial, text, integer, timestamp } from "drizzle-orm/pg-core";
import { eq, gt } from "drizzle-orm";
 
export const users = pgTable("users", {
    id: serial("id").primaryKey(),
    email: text("email").notNull().unique(),
    age: integer("age").notNull(),
    createdAt: timestamp("created_at").defaultNow(),
});
 
const db = drizzle(pool);
 
const adults = await db.select().from(users).where(gt(users.age, 18));
//    ^? { id: number; email: string; age: number; createdAt: Date }[]
 
await db.insert(users).values({ email: "x@y.com", age: 25 });

Citations