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(orswc/esbuild/oxc/Babelwith 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.tsambient types. - Compatibility: TS tracks ECMAScript closely;
targetcontrols JS output (ES2015..ESNext).libcontrols which built-ins the type system knows about. - Release cadence: ~3-month minor releases;
@beta/@rctags 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,decoratorsif 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 typescriptFor 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.tsdirectly 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.tsnatively.
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;
inferextracts 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 traceDirthen open in Chromechrome://tracing— visualizes per-file checker time, finds the offending generic / conditional type.tsc --diagnostics --listFilesto see which files are being included; common cause of slow checks is accidentally pulling all ofnode_modulesvia wildcardinclude.- Incremental builds:
tsc -b --incremental—.tsbuildinfocaches the type-checker state, second runs are typically 10-50x faster. - Project references for monorepos —
references: [{ path: "../shared" }]letstsc -brebuild only changed projects in topo order, parallelizing across cores. skipLibCheck: truefor bignode_modules— skips checking.d.tsfiles (your code still gets fully checked). Universal recommendation; 30-70% faster.moduleResolution: "bundler"(5.0+) — for Vite/esbuild/Bun consumers; skips Node’s awkward.jsextension dance.tsperf(Microsoft) and@typescript/analyze-trace— postprocess--generateTraceoutput, 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, letsswc/oxc/esbuildemit.d.tsfiles without invokingtsc. 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 numberconst 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:
--isolatedModulesis required when using non-tsc transpilers; flags constructs they can’t handle alone.--verbatimModuleSyntax(5.0+) — emit imports exactly as written, removing theimport type/importambiguity.- 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-typesfor syntax-only,--experimental-transform-typesfor 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; } // invariantUse 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:
camelCasefor vars/functions,PascalCasefor classes/types/interfaces/enums. NoI-prefix on interfaces (Microsoft’s own guidance contradicts old C# habit). - Formatters: Prettier, Biome, dprint — all support TS.
- Linters:
@typescript-eslintwitheslint, 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:
interfacefor object/class shapes you might extend,typefor unions, intersections, mapped/conditional types. Both work for most cases.- Discriminated unions over class hierarchies for state machines.
unknownnotanyat I/O boundaries; narrow with type guards or Zod parse.as constto lock literal types in fixtures and config.satisfiesfor “this matches a shape but keep my narrower type.”- Avoid
as(type assertions) — almost always means a missing guard or a bad type. - Prefer
readonlyarrays andReadonly<T>for inputs.
- Reviewer tells: stray
any, missingawait,// @ts-ignorewithout explanation (use@ts-expect-errorso it errors when fixed),Object(useobjector a shape),Function(use a specific signature),enum(prefer literal unions oras constobjects).
Ecosystem
| Domain | Tools |
|---|---|
| Frontend | React, Vue, Svelte, Solid, Angular (TS-first), Lit, Qwik |
| Meta-frameworks | Next.js, Nuxt, SvelteKit, Remix, Astro, Qwik City |
| Backend | NestJS, Fastify (TS-friendly), Hono, ElysiaJS, AdonisJS, tRPC, Encore |
| Schema / validation | Zod, Valibot, ArkType, TypeBox, Effect/Schema, io-ts, Yup |
| ORM | Prisma, Drizzle, Kysely, MikroORM, TypeORM |
| Build | Vite, esbuild, swc, tsc, Turbopack, Rolldown, oxc |
| Test | Vitest (TS-first), Jest, node:test, Playwright, Mocha |
| Monorepo | Nx, Turborepo, Moon, pnpm workspaces, Bun workspaces |
| State | TanStack Query, Zustand, Jotai, Redux Toolkit, MobX |
| LLM SDKs | OpenAI, Anthropic, Vercel AI SDK, LangChain.js, Mastra |
| Notable users | Microsoft (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):
| Lib | Bundle | Speed | Style | Notes |
|---|---|---|---|---|
| Zod 4 | ~12 KB | medium | builder | Most popular; z.infer<T>; v4 dropped IE11 baggage, 4x faster than v3 |
| Valibot | ~3 KB tree-shaken | fast | functional | Designed for bundle size; chainable + pipe; v1.0 (2025) |
| ArkType | ~25 KB | fastest | string DSL | type({ name: "string", age: "number"}); compiles to optimized validators |
| TypeBox | ~10 KB | fast | JSON Schema | Output is real JSON Schema — direct OpenAPI export |
| Effect/Schema | (with effect-ts) | fast | functional | Encoders + decoders, error tracking via Effect |
| io-ts | small | slow | fp-ts style | Legacy; superseded by Effect/Schema |
| Yup | medium | slow | builder | React-Hook-Form default; older |
| Joi | large | medium | builder | Server-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 codegen — openapi-typescript, orval, kubb generate types from OpenAPI spec.
- GraphQL — graphql-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
| Tool | Speed vs tsc | TS handling | When |
|---|---|---|---|
| tsc | 1x baseline | full check + emit | Type-checking gate, lib .d.ts emit |
| tsc (TS 7 Go) | 10x | full check + emit | Replacement for tsc when GA |
| swc | 20-30x | strip only | Production builds via Next.js, Parcel |
| esbuild | 50-100x | strip only | Vite dev, library builds with isolatedDeclarations |
| oxc | 50-100x | strip only | Rolldown internals; oxlint for linting |
| bun | 30-100x | strip + run | Dev/test runner with native execution |
| deno | n/a (V8 + swc) | strip + run | deno 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.
instanceofonly works on classes; for shapes you need a runtime validator (Zod et al.). anyis contagious — once it touches a value, all derived values becomeany. Preferunknown.- 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 butfn(obj)may not. enum— emits runtime objects, has reverse-mappings, doesn’t tree-shake well. Preferas constobjects + literal unions.neverpropagates —Promise<never>is a Promise that never resolves; check your generics.- Module resolution surprises —
nodevsnode10vsnode16vsbundlervsnodenext— pick deliberately. .jsextension in TS source when emitting ESM — required by Node ESM resolver, looks weird at first.pathsaliases without a runtime resolver (tsx, ts-node, or bundler) will fail at runtime.useDefineForClassFieldsflips 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 {}andclass 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.
readonlyis shallow —readonly T[]lets you mutateT’s fields, just not reassign the array. UseDeepReadonly<T>fromtype-festor recursiveReadonly<T>.Record<string, T>lies — accessingobj["nonexistent"]returnsT, notT | undefined. EnablenoUncheckedIndexedAccess: true.Array.prototype.includesis overly strict —["a"].includes(x)requiresx: "a", notx: string. Common workaround: assertion oras 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.jsonextendsresolution — 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
nodevsnode16vsnodenextvsbundler— different rules for.js/.tsextensions, exports field handling, conditional exports.nodenextis 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)
| Version | Released | Headline |
|---|---|---|
| 5.0 | Mar 2023 | Stage 3 decorators, const type params, extends on type params, —moduleResolution bundler, all-enum constants |
| 5.1 | Jun 2023 | Implicit returns of undefined, easier JSX element types, namespaced JSX attributes |
| 5.2 | Aug 2023 | using and await using declarations, decorator metadata, named/anonymous tuples |
| 5.3 | Nov 2023 | Import attributes (with { type: "json" }), narrowing on instanceof with Symbol.hasInstance |
| 5.4 | Mar 2024 | NoInfer<T>, preserved narrowings in closures, Object.groupBy/Map.groupBy types |
| 5.5 | Jun 2024 | Inferred type predicates, control-flow narrowed indexed accesses, ${configDir} in tsconfig, isolatedDeclarations |
| 5.6 | Sep 2024 | Disallowed nullish/truthy checks always-true diagnostics, ArrayBuffers types, region-prioritized diagnostics |
| 5.7 | Nov 2024 | Never-initialized variables errors, path rewriting for relative imports, --target esnext |
| 5.8 | Mar 2025 | Granular --erasableSyntaxOnly, search-by-type imports |
| 6.0 | 2025 | Last JS-implemented compiler line |
| 7.0 | 2026 (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 inputZod 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
- Official handbook: https://www.typescriptlang.org/docs/handbook/intro.html
- Release notes index: https://www.typescriptlang.org/docs/handbook/release-notes/overview.html
- TS 7 Beta announcement: https://devblogs.microsoft.com/typescript/announcing-typescript-7-beta/
- TS 6.0 release notes: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-6-0.html
- Compiler API: https://github.com/Microsoft/TypeScript/wiki/Using-the-Compiler-API
- DefinitelyTyped: https://github.com/DefinitelyTyped/DefinitelyTyped
- Google TS style guide: https://google.github.io/styleguide/tsguide.html
- Type Challenges: https://github.com/type-challenges/type-challenges
- TC39 type annotations proposal: https://github.com/tc39/proposal-type-annotations
- Wikipedia (version history reference): https://en.wikipedia.org/wiki/TypeScript