TypeScript FAQ
Practical questions & answers from basic types to advanced type-level programming
100 Questions • 12 CategoriesTypeScript analyzes your code before execution. It flags type mismatches, missing properties, wrong argument counts, and calls on null at compile time — when fixing them is cheap. JavaScript defers all of this to runtime, where failures appear as cryptic TypeError crashes in production.
// JavaScript — silently produces NaN at runtime
function add(a, b) { return a + b; }
add("5", 3); // "53" — string concatenation, not addition
// TypeScript — error at compile time
function add(a: number, b: number): number { return a + b; }
add("5", 3);
// ~~~ Argument of type 'string' is not assignable to parameter of type 'number'
// Property access on null — JavaScript crashes at runtime
const user = getUser(); // might return null
console.log(user.name); // TypeError: Cannot read properties of null
// TypeScript with strict null checks catches it upfront
const user = getUser(); // User | null
console.log(user.name); // Error: 'user' is possibly 'null'
console.log(user?.name); // ✅ optional chainingThe TypeScript compiler (tsc) strips all type annotations and emits plain JavaScript. No types survive to runtime — they are purely a developer tool. The compiler can also downlevel syntax (e.g., async/await to promises) based on your target setting.
// Input: user.ts
interface User { id: number; name: string; }
function greet(user: User): string {
return `Hello, ${user.name}!`;
}
const admin: User = { id: 1, name: 'Alice' };
// Output: user.js (all types erased)
function greet(user) {
return `Hello, ${user.name}!`;
}
const admin = { id: 1, name: 'Alice' };
// Compile & watch
// tsc --outDir dist --watch
// Or via tsconfig.json — just run: tsctsconfig.json is the project manifest for the TypeScript compiler. It sets target (output JS version), module (module format), strict (safety flags), outDir (output folder), and include/exclude (which files to compile). Every project should have one.
{
"compilerOptions": {
"target": "ES2022", // output JS version
"module": "NodeNext", // module system (ESM for Node 18+)
"moduleResolution": "NodeNext",
"outDir": "./dist", // compiled output folder
"rootDir": "./src", // source files root
"strict": true, // enables all strict checks
"esModuleInterop": true, // allows: import express from 'express'
"skipLibCheck": true, // skip type-checking .d.ts files (faster)
"declaration": true, // emit .d.ts files (for library authors)
"sourceMap": true // enable source maps for debugging
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}"strict": true is a shorthand that enables seven compiler flags. The most impactful are strictNullChecks (eliminates the billion-dollar mistake) and noImplicitAny (forces you to annotate untyped parameters). Always enable strict on new projects.
// strict: true enables these flags:
// ✓ strictNullChecks — null/undefined are not assignable to other types
// ✓ noImplicitAny — implicit 'any' is an error
// ✓ strictFunctionTypes — function parameters checked contravariantly
// ✓ strictBindCallApply — bind/call/apply are typed correctly
// ✓ strictPropertyInitialization — class props must be initialized
// ✓ noImplicitThis — 'this' with implicit 'any' is an error
// ✓ useUnknownInCatchVariables — catch clause variables are 'unknown' not 'any'
// Without strictNullChecks:
let name: string = null; // ✅ allowed (dangerous!)
// With strictNullChecks:
let name: string = null; // ❌ Type 'null' is not assignable to type 'string'
let name: string | null = null; // ✅ explicit nullableTypeScript uses structural (duck) typing: two types are compatible if they have the same shape, regardless of what they’re called. This contrasts with nominal typing (C#, Java) where two types with identical shapes are distinct if they have different names. Structural typing lets you pass any object with the right properties.
interface Point2D { x: number; y: number; }
interface Vector2D { x: number; y: number; }
// TypeScript: these are identical — same shape
const p: Point2D = { x: 1, y: 2 };
const v: Vector2D = p; // ✅ no error — structural match
// Extra properties are fine for function arguments
function distance(p: Point2D) { return Math.sqrt(p.x**2 + p.y**2); }
const point3D = { x: 3, y: 4, z: 5 };
distance(point3D); // ✅ point3D has at least the required shape
// Object literals ARE checked for excess properties
distance({ x: 3, y: 4, z: 5 });
// ~~~~~ Object literal may only specify known propertiesTypeScript infers types from assigned values, function return statements, and context. You only need to annotate when inference can’t figure out the type (function parameters, ambiguous initializers, complex generics). Over-annotating makes code noisy without safety benefit.
// TypeScript infers all of these — no annotation needed
const count = 0; // number
const name = 'Alice'; // string
const active = true; // boolean
const items = [1, 2, 3]; // number[]
const user = { id: 1, name: 'Bob' }; // { id: number; name: string }
// Function return types are inferred
function double(n: number) { return n * 2; } // inferred: number
const result = double(5); // inferred: number
// Annotate parameters (inference can't work backward)
function greet(name: string) { return `Hi ${name}`; }
// Annotate when you want a wider type
const status: string = 'active'; // string, not 'active'
const flag = true as boolean; // boolean, not trueSet "allowJs": true and "checkJs": false so TypeScript compiles your JS files without complaining. Rename files to .ts one at a time. Use "noImplicitAny": false initially to avoid fixing all untyped params at once. Progressively tighten strictness as the codebase improves.
// Phase 1 — TypeScript compiles JS, no errors enforced
{
"compilerOptions": {
"allowJs": true,
"checkJs": false,
"strict": false,
"noImplicitAny": false
}
}
// Phase 2 — turn on JS checking for inline type hints
// "checkJs": true
// Add JSDoc comments in JS files: /** @type {string} */
// Phase 3 — rename files to .ts, fix errors
// Enable strict incrementally:
// "strictNullChecks": true (biggest wins)
// "noImplicitAny": true (forces explicit types)
// Migration helper: ts-migrate
// npx ts-migrate migrate ./srcany opts out of type checking entirely — unsafe escape hatch. unknown is the type-safe alternative: it accepts any value but requires a type check before use. never represents the bottom type — a value that can never exist (exhausted union branches, functions that always throw).
// any — disables type checking (avoid)
let a: any = 42;
a.toUpperCase(); // no error — TypeScript looks away
// unknown — safe top type — must narrow before use
let u: unknown = fetchData();
u.toUpperCase(); // ❌ Object is of type 'unknown'
if (typeof u === 'string') u.toUpperCase(); // ✅ narrowed to string
// never — bottom type — value that cannot exist
function fail(msg: string): never { throw new Error(msg); }
// Exhaustiveness check with never
type Shape = 'circle' | 'square';
function area(s: Shape): number {
if (s === 'circle') return Math.PI;
if (s === 'square') return 1;
const _: never = s; // compile error if a new Shape is added without handling it
return _;
}A union type A | B means the value is either A or B. TypeScript tracks which branch of a union you’re in via narrowing. Unions replace overloaded functions, optional props, and error/success result patterns cleanly.
type ID = string | number;
function formatId(id: ID): string {
if (typeof id === 'string') return id.toUpperCase(); // narrowed to string
return id.toFixed(0); // narrowed to number
}
// Result type pattern — replaces throw/catch
type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };
function parseJson(raw: string): Result<unknown> {
try { return { ok: true, value: JSON.parse(raw) }; }
catch { return { ok: false, error: new Error('Invalid JSON') }; }
}
const res = parseJson('{"name":"Alice"}');
if (res.ok) console.log(res.value); // ✅ value is available
else console.error(res.error); // ✅ error is availableLiteral types are exact value types: 'GET', 200, true. Combining them into unions creates exhaustive enumerations without the runtime overhead of enum. They’re inferred from const declarations and can be widened with type annotations.
type Method = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
type Status = 200 | 201 | 400 | 401 | 403 | 404 | 500;
type Direction = 'north' | 'south' | 'east' | 'west';
function request(url: string, method: Method) { /* ... */ }
request('/api/users', 'GET'); // ✅
request('/api/users', 'FETCH'); // ❌ not a valid Method
// const infers literal type
const method = 'GET'; // type: 'GET' (literal)
let method2 = 'GET'; // type: string (widened — let can be reassigned)
// as const — preserve literal types in objects/arrays
const config = { method: 'GET', timeout: 5000 } as const;
// config.method: 'GET' (not string)
// config.timeout: 5000 (not number)Tuples are typed arrays where each position has a specific type and the length is fixed. They’re useful for function return values, coordinate pairs, and the pattern used by useState (returns [value, setter]).
type Pair = [string, number]; // fixed: [name, age]
type RGB = [number, number, number]; // three numbers
type MinMax = [min: number, max: number]; // named tuple elements
const pair: Pair = ['Alice', 30];
const [name, age] = pair; // name: string, age: number
// Tuple as function return — better than an object for simple cases
function minMax(nums: number[]): [number, number] {
return [Math.min(...nums), Math.max(...nums)];
}
const [min, max] = minMax([3, 1, 4, 1, 5]);
// Optional tuple elements
type OptPair = [string, number?]; // second element optional
// Rest in tuples (variadic)
type WithHeader = [string, ...number[]]; // string followed by any number of numbersAPI responses, JSON.parse, and catch clause variables return unknown. Unlike any, you can’t access properties on unknown until you prove the type via narrowing or a type guard. This forces intentional handling of data whose shape you don’t control.
// API response — unknown shape
async function fetchUser(id: number): Promise<unknown> {
const res = await fetch(`/api/users/${id}`);
return res.json(); // json() returns Promise<any> — cast to unknown for safety
}
const data = await fetchUser(1);
// data.name ❌ — Object is of type 'unknown'
// Option 1: type assertion (trust yourself)
const user = data as { name: string; email: string };
// Option 2: runtime validation with type guard (safe)
function isUser(v: unknown): v is { name: string; email: string } {
return typeof v === 'object' && v !== null
&& 'name' in v && typeof (v as any).name === 'string';
}
if (isUser(data)) console.log(data.name); // ✅ narrowed
// catch clause: use unknown (strict) not any
try { JSON.parse('{bad}'); }
catch (err: unknown) {
if (err instanceof Error) console.error(err.message);
}Template literal types use the same `${}` syntax as runtime template literals but at the type level. TypeScript distributes over union members, generating all combinations. Used heavily in library typings (event names, CSS properties, URL patterns).
type EventName = 'click' | 'focus' | 'blur';
type Handler = `on${Capitalize<EventName>}`;
// 'onClick' | 'onFocus' | 'onBlur'
type Axis = 'x' | 'y';
type Padding = `padding${Capitalize<Axis>}`;
// 'paddingX' | 'paddingY'
// Typed event emitter
type Events = { userCreated: { id: number }; userDeleted: { id: number } };
type EventKey = keyof Events;
// 'userCreated' | 'userDeleted'
type ListenerMap = { [K in EventKey as `on${Capitalize<K>}`]: (data: Events[K]) => void };
// { onUserCreated: (data: {id:number}) => void; onUserDeleted: ... }
// Getter/setter pair generation
type Getters<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K] };satisfies checks that an expression matches a type but infers the narrowest type, not the declared type. This lets you validate shape while keeping access to literal types and specific properties that would be lost with a type annotation.
type Palette = Record<string, string | [number, number, number]>;
// With annotation — type is widened to Palette, literals lost
const p1: Palette = { red: [255, 0, 0], green: '#00ff00' };
p1.red; // string | [number, number, number] — lost tuple type
p1.green; // string | [number, number, number] — lost string literal
// With satisfies — validated AND preserves exact inferred types
const p2 = {
red: [255, 0, 0],
green: '#00ff00'
} satisfies Palette;
p2.red; // [number, number, number] ✅ — tuple preserved
p2.green; // string ✅ — string preserved
p2.red[0]; // ✅ — can access index
// Error if shape doesn't match
const bad = { red: 'not-a-color', blue: 42 } satisfies Palette; // ✅
// { bluu: '#0000ff' } satisfies Palette ← allowed (Palette allows any string key)Regular enum compiles to a real JavaScript object — it exists at runtime. const enum is inlined at compile time (no runtime object). Literal union types are the modern preference: no runtime cost, more explicit, work across module boundaries, and don’t need special handling with isolatedModules.
// Regular enum — compiles to JS object
enum Direction { Up = 'UP', Down = 'DOWN', Left = 'LEFT', Right = 'RIGHT' }
Direction.Up; // 'UP' at runtime
// const enum — inlined, no runtime object
const enum Status { Active = 'ACTIVE', Inactive = 'INACTIVE' }
// Status.Active → 'ACTIVE' (inlined), no JS emitted
// Preferred: union of string literals
type Direction = 'UP' | 'DOWN' | 'LEFT' | 'RIGHT';
type Status = 'ACTIVE' | 'INACTIVE';
// With a const object for runtime access (best of both)
const Direction = { Up: 'UP', Down: 'DOWN' } as const;
type Direction = typeof Direction[keyof typeof Direction];
// Type: 'UP' | 'DOWN'
Direction.Up; // 'UP' — runtime value availableOptional properties (prop?: T) mean the key may be absent from the object. Properties typed as T | undefined require the key to be present, but its value may be undefined. With exactOptionalPropertyTypes enabled, these become strictly distinct.
interface A { label?: string } // key may be absent
interface B { label: string | undefined } // key must exist, value may be undefined
const a1: A = {}; // ✅ no label key
const b1: B = {}; // ❌ Property 'label' is missing
const b2: B = { label: undefined }; // ✅
// exactOptionalPropertyTypes (strict tsconfig) enforces this distinction
interface Config { timeout?: number }
const cfg: Config = { timeout: undefined }; // ❌ with exactOptionalPropertyTypes
// undefined is not assignable to number — the key should simply be omitted
// Check presence with 'in' operator
if ('label' in a1) {
a1.label; // string — narrowed, key exists
}Both describe object shapes, but they have different capabilities. Interfaces support declaration merging and are open-ended (can be extended later). Type aliases handle unions, intersections, mapped types, and conditional types — things interfaces can’t express. For object shapes: interfaces; for everything else: types.
// Interface — can be extended with declaration merging
interface User { id: number; name: string; }
interface User { email: string; } // ✅ merges — User now has id, name, email
// Type alias — cannot be reopened
type User = { id: number; name: string; };
type User = { email: string; }; // ❌ Duplicate identifier 'User'
// Extending
interface Admin extends User { role: 'admin'; } // interface extension
type Admin = User & { role: 'admin' }; // type intersection
// Only type aliases can express:
type ID = string | number; // union
type Nullable<T> = T | null; // generic alias
type Keys = keyof User; // mapped
type IsStr = string extends unknown ? true : false; // conditionalIf you redeclare an interface in the same (or a merged) scope, TypeScript merges the definitions. This is how you add properties to library types like Express’s Request or the global Window without forking the library’s types.
// types/express.d.ts — augment Express Request with custom properties
declare global {
namespace Express {
interface Request {
user?: { id: number; role: string };
}
}
}
// Now available everywhere in Express middleware
app.use((req, res, next) => {
req.user = { id: 1, role: 'admin' }; // ✅ no TypeScript error
next();
});
// Augmenting Window
declare global {
interface Window { analytics: { track: (event: string) => void }; }
}
window.analytics.track('page_view'); // ✅An index signature [key: string]: V says any string key maps to type V. This accommodates objects whose keys aren’t known at compile time (dictionaries, maps, config objects). Note that all explicit properties must also match the index signature’s value type.
// String index signature
interface StringMap { [key: string]: string; }
interface NumberMap { [key: string]: number; }
interface Cache<T> { [key: string]: T; }
const headers: StringMap = { 'Content-Type': 'application/json' };
headers['X-Request-Id'] = 'abc'; // any string key is valid
// Explicit properties must match the index value type
interface Config {
[key: string]: string | number; // value must be string | number
timeout: number; // ✅ number matches
name: string; // ✅ string matches
active: boolean; // ❌ boolean doesn't match string | number
}
// Prefer Record<string, T> for simple dictionaries
type Scores = Record<string, number>;
const s: Scores = { alice: 95, bob: 88 };readonly on a property prevents reassignment after initialization. It only enforces immutability at the TypeScript level — the compiled JS has no such protection. Use Readonly<T> and ReadonlyArray<T> (or readonly T[]) to make entire types immutable.
interface User {
readonly id: number; // can be set once, not reassigned
name: string;
}
const user: User = { id: 1, name: 'Alice' };
user.name = 'Bob'; // ✅
user.id = 2; // ❌ Cannot assign to 'id' — it is a read-only property
// ReadonlyArray — prevents push, pop, sort, etc.
const nums: readonly number[] = [1, 2, 3];
nums.push(4); // ❌ Property 'push' does not exist on type 'readonly number[]'
// Readonly utility type — makes all properties readonly
type FrozenUser = Readonly<User>;
// Deep readonly (TypeScript 4.9+)
type DeepReadonly<T> = { readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K] };An intersection type A & B requires a value to satisfy both A and B simultaneously. Properties from both types are merged. Useful for mixins, augmenting types, and combining domain models with metadata (like database records with timestamps).
type Timestamped = { createdAt: Date; updatedAt: Date };
type SoftDelete = { deletedAt: Date | null };
type User = { id: number; name: string; email: string };
type UserRecord = User & Timestamped & SoftDelete;
const user: UserRecord = {
id: 1, name: 'Alice', email: 'alice@example.com',
createdAt: new Date(), updatedAt: new Date(), deletedAt: null
};
// Mixin pattern — compose behavior
function withTimestamps<T extends object>(obj: T): T & Timestamped {
return { ...obj, createdAt: new Date(), updatedAt: new Date() };
}
// Incompatible intersections become never
type Never = string & number; // never — a value can't be both string and numberMapped types iterate over the keys of a type and produce a new type with transformed keys or values. They’re the foundation of all utility types (Partial, Required, Readonly, Record). Combine with as to rename keys.
type User = { id: number; name: string; email: string };
// Make all properties optional (same as Partial<T>)
type Optional<T> = { [K in keyof T]?: T[K] };
// Make all properties nullable
type Nullable<T> = { [K in keyof T]: T[K] | null };
// Remove readonly from all properties (same as -readonly pattern)
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
// Rename keys with 'as' clause + template literals
type Getters<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K] };
type UserGetters = Getters<User>;
// { getId: () => number; getName: () => string; getEmail: () => string }Conditional types use T extends U ? X : Y syntax — like a ternary but at the type level. When applied to a union type, they distribute over each member. Used in utility types like NonNullable, Extract, and Exclude.
// Basic conditional type
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
// Distributed over union
type C = IsString<string | number>; // boolean (true | false)
// NonNullable — remove null and undefined from union
type NonNullable<T> = T extends null | undefined ? never : T;
type D = NonNullable<string | null | undefined>; // string
// Flatten array type
type Flatten<T> = T extends Array<infer Item> ? Item : T;
type E = Flatten<string[]>; // string
type F = Flatten<number>; // number
// Exclude — remove members assignable to U
type Exclude<T, U> = T extends U ? never : T;
type G = Exclude<'a' | 'b' | 'c', 'a'>; // 'b' | 'c'infer declares a type variable to be inferred from a pattern in a conditional type. It lets you “capture” part of a type — the element type of an array, the return type of a function, the resolved type of a Promise — without knowing the specific type upfront.
// Extract array element type
type ElementOf<T> = T extends Array<infer E> ? E : never;
type Nums = ElementOf<number[]>; // number
type Strs = ElementOf<string[]>; // string
// Extract function return type (same as ReturnType<T>)
type MyReturn<T> = T extends (...args: any[]) => infer R ? R : never;
type R1 = MyReturn<() => string>; // string
type R2 = MyReturn<(n: number) => boolean>; // boolean
// Unwrap Promise
type Awaited<T> = T extends Promise<infer V> ? Awaited<V> : T;
type V = Awaited<Promise<Promise<string>>>; // string
// Extract first argument type
type FirstArg<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;
type F = FirstArg<(name: string, age: number) => void>; // stringOverload signatures declare the different valid call forms. A single implementation signature (wider types) handles all cases. TypeScript matches the call against each overload signature in order — callers see only the overloads, not the implementation signature.
// Overload signatures (what callers see)
function format(value: string): string;
function format(value: number): string;
function format(value: Date): string;
// Implementation signature (wider — not directly callable)
function format(value: string | number | Date): string {
if (typeof value === 'string') return value.trim();
if (typeof value === 'number') return value.toLocaleString();
return value.toISOString().slice(0, 10);
}
format(' hello '); // ✅ string overload
format(42000); // ✅ number overload
format(new Date()); // ✅ Date overload
format(true); // ❌ No overload matches booleanGenerics let a function work with any type while keeping the relationship between input and output. TypeScript infers the type argument from the call — no explicit <T> needed at the call site. The result type depends on what you passed in.
// T is inferred from what's passed
function identity<T>(value: T): T { return value; }
const n = identity(42); // T inferred as number, returns number
const s = identity('hello'); // T inferred as string, returns string
// Multiple type params
function zip<A, B>(a: A[], b: B[]): [A, B][] {
return a.map((v, i) => [v, b[i]]);
}
const pairs = zip([1, 2], ['a', 'b']); // [number, string][]
// Generic with transform
function mapObject<T, U>(obj: T, fn: (value: T[keyof T]) => U): Record<keyof T, U> {
return Object.fromEntries(
Object.entries(obj as object).map(([k, v]) => [k, fn(v)])
) as Record<keyof T, U>;
}These utility types use conditional types with infer to extract parts of function types. They let you derive types from existing functions without duplicating type declarations — especially useful when the function signature is the source of truth.
async function fetchUser(id: number, options?: { cache: boolean }) {
const res = await fetch(`/api/users/${id}`);
return res.json() as Promise<{ id: number; name: string }>;
}
// Extract types from the function — no duplication
type FetchParams = Parameters<typeof fetchUser>;
// [id: number, options?: { cache: boolean } | undefined]
type FetchReturn = Awaited<ReturnType<typeof fetchUser>>;
// { id: number; name: string }
// Useful for wrappers that must match a function's signature
function withCache<T extends (...args: any[]) => Promise<any>>(fn: T) {
return (...args: Parameters<T>): ReturnType<T> => {
const key = JSON.stringify(args);
// check cache...
return fn(...args);
};
}Rest parameters are typed as arrays. Variadic tuple types (TypeScript 4.0+) let you spread type-safe tuples into rest positions, enabling precise typing of functions that forward arguments while preserving types of each positional argument.
// Rest parameter — array type
function sum(...nums: number[]): number { return nums.reduce((a, b) => a + b, 0); }
// Variadic tuples — preserve individual arg types
type Concat<T extends unknown[], U extends unknown[]> = [...T, ...U];
type AB = Concat<[string, number], [boolean]>; // [string, number, boolean]
// Strongly typed bind/curry
function bind<T, A extends unknown[], B extends unknown[], R>(
fn: (first: T, ...args: [...A, ...B]) => R,
first: T,
...partial: A
): (...remaining: B) => R {
return (...remaining: B) => fn(first, ...partial, ...remaining);
}
// Labeled tuple elements for better error messages
type Range = [start: number, end: number, step?: number];void means the return value should not be used. A function typed as () => void can actually return a value — TypeScript simply says “ignore it.” undefined as a return type means the function must return exactly undefined. This distinction matters for callback compatibility.
// void — return value ignored by caller
type VoidCallback = () => void;
const fn: VoidCallback = () => 42; // ✅ 42 is returned but ignored
const fn2: VoidCallback = () => 'hi'; // ✅ same
// This is why Array.forEach callback can be typed as void:
[1,2,3].forEach((n): void => {
return n * 2; // ✅ return value is discarded
});
// undefined — must explicitly return undefined
function mustReturnUndefined(): undefined {
return; // ✅
// return 42; // ❌ Type 'number' is not assignable to type 'undefined'
}
// Practical difference
declare function each(cb: () => void): void;
each(() => [1,2,3].push(4)); // ✅ push returns number, but void ignores itHigher-order functions use generics to preserve the type of the callback’s arguments and return value. The key is keeping the type parameter free so callers don’t have to specify it explicitly — TypeScript infers from the callback.
// Typed pipe — compose functions left to right
function pipe<A, B, C>(f: (a: A) => B, g: (b: B) => C): (a: A) => C {
return (a) => g(f(a));
}
const parse = (s: string) => parseInt(s, 10);
const double = (n: number) => n * 2;
const process = pipe(parse, double);
const result = process('21'); // result: number — inferred correctly
// Memoize — wraps any function, preserves its types
function memoize<T extends (...args: any[]) => any>(fn: T): T {
const cache = new Map<string, ReturnType<T>>();
return ((...args: Parameters<T>): ReturnType<T> => {
const key = JSON.stringify(args);
if (!cache.has(key)) cache.set(key, fn(...args));
return cache.get(key)!;
}) as T;
}
const memoFib = memoize((n: number): number => n < 2 ? n : memoFib(n-1) + memoFib(n-2));TypeScript allows a fake first parameter named this to declare what this must be when the function is called. It’s erased in the compiled output. Useful when writing methods that are passed as callbacks, ensuring this isn’t accidentally unbound.
interface Counter { count: number; increment(): void; }
// Without 'this' param — TypeScript can't check this binding
function badIncrement() { this.count++; } // implicit any
// With 'this' param — enforces correct binding
function increment(this: Counter): void { this.count++; }
const c: Counter = { count: 0, increment };
c.increment(); // ✅ this is Counter
const fn = c.increment;
fn(); // ❌ The 'this' context of type 'void' is not assignable to 'Counter'
// noImplicitThis flag catches untyped this usage
class Button {
clicked = false;
handleClick(this: Button) { this.clicked = true; }
}TypeScript checks function type compatibility with covariant return types (subtype can return a narrower type) but — with strictFunctionTypes — contravariant parameter types (callback must accept a wider type, not narrower). Passing a callback that expects a narrow type where a wide type is provided is unsound.
type Animal = { name: string };
type Dog = Animal & { breed: string };
// Return type — COVARIANT (return Dog where Animal expected: ✅)
type AnimalFactory = () => Animal;
type DogFactory = () => Dog;
const factory: AnimalFactory = (): Dog => ({ name: 'Rex', breed: 'Lab' }); // ✅
// Parameter type — CONTRAVARIANT (accept Animal where Dog handler expected: ✅)
type DogHandler = (d: Dog) => void;
type AnimalHandler = (a: Animal) => void;
const h: DogHandler = (a: Animal) => console.log(a.name); // ✅ accepts wider type
const h2: DogHandler = (d: Dog) => console.log(d.breed); // ✅ accepts exact type
// Unsound: narrow parameter handler where wide param expected
const h3: AnimalHandler = (d: Dog) => d.breed.toUpperCase(); // ❌ with strictFunctionTypesT extends SomeType in a generic declaration says T must be assignable to SomeType. This lets you safely access properties of T that you know it has. Without the constraint, accessing any property would be a type error since T could be anything.
// Without constraint — can't access any properties
function getLength<T>(val: T): number { return val.length; } // ❌ T has no 'length'
// With constraint — T must have length
function getLength<T extends { length: number }>(val: T): number { return val.length; }
getLength('hello'); // ✅ string has length
getLength([1,2,3]); // ✅ array has length
getLength({ length: 5 }); // ✅ has length property
getLength(42); // ❌ number has no 'length'
// keyof constraint — T must be a key of U
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: 'Alice', email: 'alice@example.com' };
getProperty(user, 'name'); // ✅ returns string
getProperty(user, 'age'); // ❌ 'age' not a key of user typeThese are generic mapped types built into TypeScript’s standard library. They let you derive variants of a type without repeating properties — essential for update payloads, form states, and API shapes that differ slightly from domain models.
interface User { id: number; name: string; email: string; role: 'admin' | 'user'; }
// Partial — all properties optional (PATCH request body)
type UserPatch = Partial<User>;
// { id?: number; name?: string; email?: string; role?: 'admin' | 'user' }
// Required — all properties required (removes optional modifiers)
type RequiredUser = Required<User>;
// Pick — select a subset of properties
type UserPreview = Pick<User, 'id' | 'name'>;
// { id: number; name: string }
// Omit — remove specific properties
type PublicUser = Omit<User, 'role'>;
// { id: number; name: string; email: string }
// Common pattern — create-vs-update types
type CreateUser = Omit<User, 'id'>; // no id on creation
type UpdateUser = Partial<Omit<User, 'id'>>; // all optional except idRecord<K, V> creates an object type where each key from K maps to a value of type V. When K is a union of literals, TypeScript enforces that all keys are present. When K is string, it becomes a dictionary type.
// All union members required as keys
type Status = 'active' | 'inactive' | 'pending';
type StatusConfig = Record<Status, { label: string; color: string }>;
const config: StatusConfig = {
active: { label: 'Active', color: 'green' },
inactive: { label: 'Inactive', color: 'gray' },
pending: { label: 'Pending', color: 'amber' }
// TypeScript enforces all three keys are present
};
// Dictionary with string keys
type Cache = Record<string, unknown>;
type ScoreMap = Record<string, number>;
// Equivalent to index signature but more concise
type Same = { [K in string]: number }; // same as Record<string, number>
// Record in switch-like lookup (faster than switch)
const httpMessages: Record<number, string> = {
200: 'OK', 201: 'Created', 400: 'Bad Request', 404: 'Not Found', 500: 'Internal Server Error'
};Exclude<T, U> removes members assignable to U from union T. Extract<T, U> keeps only members assignable to U. They’re the set-operations of the type system — complement and intersection over union members.
type Primitives = string | number | boolean | null | undefined;
// Exclude — remove matching members
type NonNullPrimitives = Exclude<Primitives, null | undefined>;
// string | number | boolean
// Extract — keep only matching members
type StringOrNumber = Extract<Primitives, string | number>;
// string | number
// NonNullable — built-in uses Exclude
type NonNullable<T> = Exclude<T, null | undefined>;
// Practical: filter a union of object types by discriminant
type Event = { type: 'click'; x: number } | { type: 'focus' } | { type: 'blur' };
type MouseEvent = Extract<Event, { type: 'click' }>;
// { type: 'click'; x: number }
// Remove specific literal from union
type AllSizes = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
type CommonSizes = Exclude<AllSizes, 'xs' | 'xl'>;
// 'sm' | 'md' | 'lg'A generic repository abstracts database operations so each entity type (User, Order, Product) gets a fully typed CRUD interface without duplicating the implementation. TypeScript infers the correct return types from the entity type parameter.
interface Entity { id: number }
interface Repository<T extends Entity> {
findById(id: number): Promise<T | null>;
findAll(): Promise<T[]>;
create(data: Omit<T, 'id'>): Promise<T>;
update(id: number, data: Partial<Omit<T, 'id'>>): Promise<T | null>;
delete(id: number): Promise<boolean>;
}
// Concrete implementation for any entity
class PrismaRepository<T extends Entity> implements Repository<T> {
constructor(private readonly model: any) {}
findById(id: number) { return this.model.findUnique({ where: { id } }); }
findAll() { return this.model.findMany(); }
create(data: Omit<T, 'id'>) { return this.model.create({ data }); }
update(id: number, data: Partial<Omit<T, 'id'>>) { return this.model.update({ where: { id }, data }); }
delete(id: number) { return this.model.delete({ where: { id } }).then(() => true); }
}
interface User extends Entity { name: string; email: string }
const users = new PrismaRepository<User>(prisma.user);
const u = await users.findById(1); // User | nullkeyof T produces a union of T‘s property key names. T[K] is an indexed access type — it returns the type of property K on T. Together they enable type-safe property accessors that adapt when the base type changes.
interface Product { id: number; name: string; price: number; stock: number }
type ProductKey = keyof Product; // 'id' | 'name' | 'price' | 'stock'
type ProductName = Product['name']; // string
type ProductPrice = Product['price']; // number
type ProductValues = Product[keyof Product]; // number | string
// Type-safe getter — K must be a key of T, return is T[K]
function get<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key]; }
const p = { id: 1, name: 'Widget', price: 9.99, stock: 100 };
const name = get(p, 'name'); // string
const price = get(p, 'price'); // number
const nope = get(p, 'color'); // ❌ 'color' not in Product
// Deep access
interface Config { db: { host: string; port: number }; }
type DBHost = Config['db']['host']; // stringIn type position, typeof x produces the TypeScript type of x. This is how you derive types from implementation rather than duplicating declarations — especially useful for config objects, module exports, and function signatures.
// Derive type from a const — no separate interface needed
const defaultConfig = {
host: 'localhost',
port: 5432,
ssl: false,
timeout: 30_000
} as const;
type Config = typeof defaultConfig;
// { readonly host: 'localhost'; readonly port: 5432; readonly ssl: false; readonly timeout: 30000 }
// Capture function return type from implementation
const getUser = () => ({ id: 1, name: 'Alice', email: 'alice@example.com' });
type User = ReturnType<typeof getUser>;
// { id: number; name: string; email: string }
// Module-level type from export
import * as api from './api';
type ApiModule = typeof api;
type GetUser = typeof api.getUser;When a conditional type’s checked type is a naked type parameter, TypeScript automatically applies the condition to each member of a union and produces a union of results. This is called distributive conditional types and enables powerful union transformations.
type ToArray<T> = T extends any ? T[] : never;
type StrOrNum = string | number;
type Arrays = ToArray<StrOrNum>; // string[] | number[] — distributed!
// Without distribution (wrap in tuple to prevent it)
type ToArrayND<T> = [T] extends [any] ? T[] : never;
type ArrayND = ToArrayND<StrOrNum>; // (string | number)[] — not distributed
// Practical: extract object types from a union
type Events =
| { type: 'click'; x: number; y: number }
| { type: 'keydown'; key: string }
| { type: 'scroll'; delta: number };
type EventPayload<T extends Events['type']> =
Extract<Events, { type: T }>;
type ClickEvent = EventPayload<'click'>; // { type: 'click'; x: number; y: number }TypeScript’s private/protected/public are compile-time only — they’re erased to JS and don’t prevent runtime access. JavaScript’s native #field syntax is truly private at runtime via WeakMap under the hood. Use TS modifiers for most cases; native # when runtime privacy is required.
class BankAccount {
public owner: string; // accessible everywhere (default)
private balance: number; // TS only — erased at runtime
protected id: string; // accessible in subclasses
#secret: string; // JS native — truly private at runtime
constructor(owner: string, balance: number) {
this.owner = owner;
this.balance = balance;
this.id = crypto.randomUUID();
this.#secret = 'abc123';
}
deposit(amount: number) { this.balance += amount; }
getBalance() { return this.balance; }
}
const acc = new BankAccount('Alice', 100);
acc.owner; // ✅
acc.balance; // ❌ TypeScript error (but accessible at runtime!)
(acc as any).balance; // works at runtime — TS modifiers are compile-time only
// acc.#secret // ❌ real privacy — SyntaxError at runtime tooAbstract classes can’t be instantiated directly — they define shared implementation and declare abstract methods that every subclass must implement. Use them when multiple concrete classes share logic but differ in specific behaviors (template method pattern).
abstract class Exporter {
// Concrete shared method
async export(data: unknown[]): Promise<void> {
const formatted = await this.format(data); // calls abstract method
await this.write(formatted);
}
// Abstract — subclasses must implement
abstract format(data: unknown[]): Promise<string>;
abstract write(content: string): Promise<void>;
abstract readonly mimeType: string;
}
class CsvExporter extends Exporter {
readonly mimeType = 'text/csv';
async format(data: unknown[]) { return data.map(r => Object.values(r as object).join(',')).join('\n'); }
async write(content: string) { await fs.writeFile('export.csv', content); }
}
// new Exporter() ❌ Cannot create an instance of an abstract class
const exporter: Exporter = new CsvExporter(); // ✅ via concrete classPrefixing constructor parameters with an access modifier (public, private, protected, readonly) simultaneously declares the property and assigns the argument to it. Eliminates three lines per property.
// Verbose — without shorthand
class UserService {
private readonly db: Database;
private readonly cache: Cache;
public readonly logger: Logger;
constructor(db: Database, cache: Cache, logger: Logger) {
this.db = db;
this.cache = cache;
this.logger = logger;
}
}
// Concise — with parameter properties
class UserService {
constructor(
private readonly db: Database,
private readonly cache: Cache,
public readonly logger: Logger
) {} // all properties declared and assigned automatically
async findUser(id: number) {
return this.cache.get(id) ?? await this.db.users.findById(id);
}
}implements tells TypeScript to verify that the class provides all properties and methods of the interface. It doesn’t inherit anything — it just validates the class’s shape. A class can implement multiple interfaces.
interface Serializable {
serialize(): string;
deserialize(raw: string): this;
}
interface Auditable {
readonly createdAt: Date;
readonly updatedAt: Date;
}
class Order implements Serializable, Auditable {
readonly createdAt = new Date();
readonly updatedAt = new Date();
constructor(public id: number, public total: number) {}
serialize(): string { return JSON.stringify({ id: this.id, total: this.total }); }
deserialize(raw: string): this {
const data = JSON.parse(raw);
return Object.assign(Object.create(Object.getPrototypeOf(this)), data);
}
}
// Interfaces as contracts for dependency injection
interface PaymentGateway { charge(amount: number): Promise<{ success: boolean }>; }
class StripeGateway implements PaymentGateway { /* ... */ }
class TestGateway implements PaymentGateway {
async charge() { return { success: true }; } // test double
}Static members belong to the class itself, not instances. typeof MyClass gives the type of the class constructor (the static side); the instance type is just MyClass. The singleton pattern restricts instantiation to a single shared instance.
class DatabaseConnection {
private static instance: DatabaseConnection | null = null;
private constructor(public readonly url: string) {}
static getInstance(url: string): DatabaseConnection {
if (!DatabaseConnection.instance) {
DatabaseConnection.instance = new DatabaseConnection(url);
}
return DatabaseConnection.instance;
}
static resetForTests() { DatabaseConnection.instance = null; }
}
const db1 = DatabaseConnection.getInstance('postgres://...');
const db2 = DatabaseConnection.getInstance('postgres://...');
db1 === db2; // true — same instance
// new DatabaseConnection('...') ❌ constructor is private
// Static type vs instance type
type DBClass = typeof DatabaseConnection; // constructor type
type DBInstance = InstanceType<typeof DatabaseConnection>; // instance typeDecorators are functions that wrap class declarations, methods, properties, or parameters. They run at class definition time. Enable "experimentalDecorators": true in tsconfig. Used heavily in NestJS (DI, routing), TypeORM (entity mapping), and class-validator (validation rules).
// Method decorator — logs execution time
function measureTime(target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = async function (...args: unknown[]) {
const start = performance.now();
const result = await original.apply(this, args);
console.log(`${key} took ${(performance.now() - start).toFixed(1)}ms`);
return result;
};
return descriptor;
}
// Property decorator — marks required fields
function Required(target: any, key: string) {
const requiredKeys: string[] = Reflect.getMetadata('required', target) ?? [];
Reflect.defineMetadata('required', [...requiredKeys, key], target);
}
class UserService {
@measureTime
async findAll() { return db.users.findMany(); }
}
// NestJS-style — decorator-driven DI and routing
@Injectable()
class OrderService {
constructor(private readonly db: PrismaService) {}
@Get('/orders')
async list() { return this.db.order.findMany(); }
}TypeScript getters and setters let you intercept property reads and writes. Getters can compute values on the fly; setters can validate or transform before assignment. TypeScript infers the getter’s return type and enforces that the setter accepts a compatible type.
class Temperature {
private _celsius: number = 0;
get celsius(): number { return this._celsius; }
set celsius(value: number) {
if (value < -273.15) throw new RangeError('Temperature below absolute zero');
this._celsius = value;
}
get fahrenheit(): number { return this._celsius * 9/5 + 32; }
set fahrenheit(value: number) { this.celsius = (value - 32) * 5/9; }
}
const t = new Temperature();
t.celsius = 100;
t.fahrenheit; // 212 — computed
t.fahrenheit = 32;
t.celsius; // 0 — computed
t.celsius = -300; // ❌ RangeError at runtime (validated in setter)InstanceType<T> takes a constructor type and returns the type of instances it creates. Useful when you have a reference to a class (constructor) and need to type what new Class() produces — common in factory functions and dependency injection containers.
class User { constructor(public name: string) {} }
class Order { constructor(public total: number) {} }
// Factory that takes a constructor and returns an instance
function create<T extends new (...args: any[]) => any>(
Ctor: T,
...args: ConstructorParameters<T>
): InstanceType<T> {
return new Ctor(...args);
}
const user = create(User, 'Alice'); // User
const order = create(Order, 100); // Order
// Container registry
type Constructor = new (...args: any[]) => any;
const registry = new Map<string, Constructor>();
registry.set('user', User);
function resolve<T extends Constructor>(key: string): InstanceType<T> {
return new (registry.get(key)!)();
}TypeScript’s control flow analysis tracks which types are possible at each point in the code. After a typeof x === 'string' check, x is narrowed to string in the true branch and the remaining union members in the false branch. This happens automatically — no explicit cast needed.
function process(val: string | number | boolean) {
if (typeof val === 'string') {
val.toUpperCase(); // val: string
} else if (typeof val === 'number') {
val.toFixed(2); // val: number
} else {
val; // val: boolean — only remaining type
}
}
// Narrowing with nullish checks
function greet(name: string | null | undefined) {
if (name == null) return 'Hello, stranger!'; // null | undefined handled
return `Hello, ${name.toUpperCase()}!`; // name: string
}
// Equality narrowing
function compare(x: string | number, y: string | boolean) {
if (x === y) {
x.toUpperCase(); // x: string — only string can === string | boolean
}
}A discriminated union is a union of object types that share a common literal property (the discriminant). TypeScript narrows to the exact variant when you check that property. Adding a never check in the default branch catches missing cases at compile time.
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'rectangle'; width: number; height: number }
| { kind: 'triangle'; base: number; height: number };
function area(shape: Shape): number {
switch (shape.kind) {
case 'circle': return Math.PI * shape.radius ** 2;
case 'rectangle': return shape.width * shape.height;
case 'triangle': return 0.5 * shape.base * shape.height;
default:
const _exhaustive: never = shape; // error if a new Shape variant is added without a case
return _exhaustive;
}
}
// Event union with payloads
type AppEvent =
| { type: 'LOGIN'; payload: { userId: string } }
| { type: 'LOGOUT' }
| { type: 'ORDER'; payload: { orderId: string; total: number } };A type guard is a function with a return type of x is T. When it returns true, TypeScript narrows x to T in the calling scope. Use them for runtime validation of unknown data, API responses, and complex shape checks.
interface User { id: number; name: string; role: 'user' }
interface Admin { id: number; name: string; role: 'admin'; permissions: string[] }
type Person = User | Admin;
// Type guard — narrows to Admin
function isAdmin(person: Person): person is Admin {
return person.role === 'admin';
}
function showPermissions(person: Person) {
if (isAdmin(person)) {
console.log(person.permissions); // ✅ person: Admin
}
}
// Validate unknown API response
function isUser(val: unknown): val is User {
return (
typeof val === 'object' && val !== null &&
typeof (val as any).id === 'number' &&
typeof (val as any).name === 'string' &&
(val as any).role === 'user'
);
}
const data: unknown = await fetch('/api/me').then(r => r.json());
if (isUser(data)) data.name; // ✅ narrowed to UserThe in operator checks if a property exists on an object and narrows the type based on which union members could have that property. Useful as a lightweight alternative to a full type guard when discriminating by optional properties.
type Cat = { name: string; meow(): void };
type Dog = { name: string; bark(): void };
type Pet = Cat | Dog;
function makeNoise(pet: Pet) {
if ('meow' in pet) {
pet.meow(); // pet: Cat
} else {
pet.bark(); // pet: Dog
}
}
// Narrowing optional properties
type SuccessResponse = { data: unknown; status: 'ok' };
type ErrorResponse = { error: string; code: number; status: 'error' };
type ApiResponse = SuccessResponse | ErrorResponse;
function handle(res: ApiResponse) {
if ('error' in res) {
console.error(res.error, res.code); // res: ErrorResponse
} else {
console.log(res.data); // res: SuccessResponse
}
}Assertion functions use asserts x is T (or asserts condition) as their return type. After calling them, TypeScript assumes the assertion passed — narrowing the type for the rest of the scope. They’re like type guards but throw instead of returning false.
// Asserts a condition is truthy
function assert(condition: unknown, msg: string): asserts condition {
if (!condition) throw new Error(msg);
}
// Asserts a value is a specific type
function assertIsString(val: unknown): asserts val is string {
if (typeof val !== 'string') throw new TypeError(`Expected string, got ${typeof val}`);
}
// Usage — TypeScript narrows after each assertion
function processId(id: unknown) {
assertIsString(id);
id.toUpperCase(); // ✅ id: string — guaranteed by assertion
const trimmed = id.trim();
assert(trimmed.length > 0, 'ID cannot be empty');
// trimmed is guaranteed non-empty here
return trimmed;
}
// Non-null assertion as an operator (use sparingly)
function getConfig() { return process.env.DB_URL!; } // ! = assert not null/undefinedAfter x instanceof ClassName, TypeScript narrows x to ClassName in the true branch. With inheritance, it narrows to the checked class (not the base). Useful for differentiating error types and working with class-based libraries.
class NetworkError extends Error { constructor(public statusCode: number, msg: string) { super(msg); } }
class ValidationError extends Error { constructor(public fields: string[], msg: string) { super(msg); } }
class AuthError extends Error { constructor(msg: string) { super(msg); } }
function handleError(err: unknown) {
if (err instanceof NetworkError) {
console.error(`HTTP ${err.statusCode}: ${err.message}`); // err: NetworkError
} else if (err instanceof ValidationError) {
console.error(`Invalid fields: ${err.fields.join(', ')}`); // err: ValidationError
} else if (err instanceof AuthError) {
redirect('/login');
} else if (err instanceof Error) {
console.error(`Unexpected: ${err.message}`); // err: Error
} else {
console.error('Unknown error', err);
}
}TypeScript tracks the type of a variable as it flows through assignments, conditionals, and loops. After an early return, the type is narrowed for the rest of the function. After assignment, the variable takes the assigned type — even mid-function.
function process(input: string | null) {
// Early return pattern — no else needed
if (input === null) return;
input.toUpperCase(); // input: string — null eliminated
// Reassignment narrows to the new type
let value: string | number = 'hello';
value.toUpperCase(); // value: string
value = 42;
value.toFixed(2); // value: number — narrowed by assignment
// Loop narrowing
let count: number | null = null;
for (const item of [1, 2, 3]) {
count = item;
count.toFixed(); // count: number — narrowed inside loop body
}
count; // number | null — TypeScript accounts for empty array
}Because TypeScript is structural, UserId and OrderId typed as number are interchangeable — a bug waiting to happen. Branded (nominal) types use an intersection with a unique phantom property to make them structurally distinct without any runtime cost.
// Brand utility
type Brand<T, B extends string> = T & { readonly __brand: B };
type UserId = Brand<number, 'UserId'>;
type OrderId = Brand<number, 'OrderId'>;
// Constructors that produce branded values
const UserId = (n: number): UserId => n as UserId;
const OrderId = (n: number): OrderId => n as OrderId;
function getUser(id: UserId) { return db.users.findById(id); }
function getOrder(id: OrderId){ return db.orders.findById(id); }
const uid = UserId(1);
const oid = OrderId(42);
getUser(uid); // ✅
getUser(oid); // ❌ OrderId not assignable to UserId — prevented!
getUser(1); // ❌ plain number not assignable to UserId
// Also useful for: EmailAddress, Url, NonEmptyString, PositiveNumberA type can reference itself in its definition to model arbitrarily nested structures — JSON values, trees, file systems, menus. TypeScript supports recursive type aliases with interfaces or deferred recursive references.
// Recursive JSON type
type Json =
| string | number | boolean | null
| Json[]
| { [key: string]: Json };
const data: Json = { users: [{ id: 1, active: true }], total: null };
// Tree node
interface TreeNode<T> {
value: T;
children: TreeNode<T>[];
}
const tree: TreeNode<string> = {
value: 'root',
children: [
{ value: 'a', children: [] },
{ value: 'b', children: [{ value: 'b1', children: [] }] }
]
};
// Deep readonly (recursive mapped type)
type DeepReadonly<T> =
T extends (infer U)[] ? ReadonlyArray<DeepReadonly<U>> :
T extends object ? { readonly [K in keyof T]: DeepReadonly<T[K]> } :
T;as const tells TypeScript to infer the narrowest possible type: string literals instead of string, number literals instead of number, and readonly tuples instead of mutable arrays. Essential for deriving union types from data.
// Without as const — widened types
const config = { method: 'GET', timeout: 5000 };
// { method: string; timeout: number }
// With as const — literal types, readonly
const config = { method: 'GET', timeout: 5000 } as const;
// { readonly method: 'GET'; readonly timeout: 5000 }
// Derive a union from an array
const ROLES = ['admin', 'user', 'moderator'] as const;
type Role = typeof ROLES[number]; // 'admin' | 'user' | 'moderator'
// Enum-like object with derived type
const STATUS = { Active: 'ACTIVE', Inactive: 'INACTIVE', Pending: 'PENDING' } as const;
type Status = typeof STATUS[keyof typeof STATUS]; // 'ACTIVE' | 'INACTIVE' | 'PENDING'
STATUS.Active; // 'ACTIVE' — runtime value
// Routes lookup with as const
const ROUTES = { home: '/', dashboard: '/dashboard', profile: '/profile' } as const;
type Route = typeof ROUTES[keyof typeof ROUTES]; // '/' | '/dashboard' | '/profile'TypeScript 5.4 added NoInfer<T>. When a parameter is wrapped in NoInfer, TypeScript ignores it for type inference purposes — forcing inference from other parameters. This prevents a “default value” parameter from widening the inferred type.
// Problem: TypeScript infers T from both args, widening to string | number
function createState<T>(initial: T, fallback: T): T {
return initial ?? fallback;
}
const s = createState('active', 42); // T inferred as string | number — not useful!
// Solution: NoInfer — infer T only from 'initial', not 'fallback'
function createState<T>(initial: T, fallback: NoInfer<T>): T {
return initial ?? fallback;
}
const s2 = createState('active', 42); // ❌ 42 not assignable to string — correct!
const s3 = createState('active', 'idle'); // ✅ T = string
// Common use: restrict a default value to match inferred type
function pick<T, K extends keyof T>(obj: T, key: K, fallback: NoInfer<T[K]>): T[K] {
return obj[key] ?? fallback;
}The as clause in a mapped type lets you remap each key to a new name (or to never to filter it out). This enables creating derivative types that include or exclude properties based on their types or names.
interface Model {
id: number;
name: string;
createdAt: Date;
updatedAt: Date;
deletedAt: Date | null;
}
// Keep only Date properties
type DateFields<T> = {
[K in keyof T as T[K] extends Date | null ? K : never]: T[K]
};
type ModelDates = DateFields<Model>;
// { createdAt: Date; updatedAt: Date; deletedAt: Date | null }
// Prefix all keys
type Prefixed<T, P extends string> = {
[K in keyof T as `${P}${Capitalize<string & K>}`]: T[K]
};
type PrefixedModel = Prefixed<{ name: string; age: number }, 'user'>;
// { userName: string; userAge: number }
// Omit by value type (remove function properties)
type DataOnly<T> = { [K in keyof T as T[K] extends Function ? never : K]: T[K] };Variadic tuple types (TypeScript 4.0+) allow ...T spreads within tuple types where T is an array/tuple type parameter. This enables typing functions like concat, bind, and curried pipelines where the number and types of arguments are statically known.
// Concatenate two tuples
type Concat<T extends unknown[], U extends unknown[]> = [...T, ...U];
type ABC = Concat<[string, number], [boolean, Date]>;
// [string, number, boolean, Date]
// Prepend element to tuple
type Prepend<T extends unknown[], V> = [V, ...T];
type P = Prepend<[number, boolean], string>; // [string, number, boolean]
// Typed partial application
function partial<A extends unknown[], B extends unknown[], R>(
fn: (...args: [...A, ...B]) => R,
...head: A
): (...tail: B) => R {
return (...tail: B) => fn(...head, ...tail);
}
function add3(a: number, b: number, c: number) { return a + b + c; }
const add10 = partial(add3, 10); // (...tail: [number, number]) => number
add10(2, 3); // 15 — TypeScript knows it needs 2 more numbersAwaited<T> (TypeScript 4.5+) recursively resolves Promise and PromiseLike chains to the final value type. It handles Promise<Promise<string>> correctly — previous versions of ReturnType/infer patterns only unwrapped one level.
type A = Awaited<Promise<string>>; // string
type B = Awaited<Promise<Promise<number>>>; // number — recursively unwrapped
type C = Awaited<Promise<string | number>>; // string | number
// Practical: get the resolved type of an async function
async function fetchUser(id: number) {
const res = await fetch(`/api/users/${id}`);
return res.json() as Promise<{ id: number; name: string }>;
}
type UserResult = Awaited<ReturnType<typeof fetchUser>>;
// { id: number; name: string }
// Useful when composing async pipelines
type ResolvedAll<T extends readonly Promise<unknown>[]> = {
[K in keyof T]: Awaited<T[K]>
};
// Mimics the return type of Promise.allThe accessor keyword defines an auto-accessor: it generates a private backing field plus getter and setter automatically. Decorators using the Stage 3 decorator standard can intercept accessor reads and writes — essential for reactive frameworks and ORM field tracking.
// accessor generates private #name field + get name()/set name() automatically
class Person {
accessor name: string;
constructor(name: string) { this.name = name; }
}
// Equivalent to:
class PersonManual {
#name: string = '';
get name() { return this.#name; }
set name(val: string) { this.#name = val; }
constructor(name: string) { this.name = name; }
}
// Decorator that intercepts accessor reads/writes (Stage 3 decorators)
function logged<T>(target: ClassAccessorDecoratorTarget<unknown, T>, ctx: ClassAccessorDecoratorContext) {
return {
get(this: unknown) { const v = target.get.call(this); console.log(`get ${String(ctx.name)}:`, v); return v; },
set(this: unknown, val: T){ console.log(`set ${String(ctx.name)}:`, val); target.set.call(this, val); }
};
}
class Config {
@logged accessor timeout: number = 5000;
}Zod is a schema validation library where the runtime schema is the single source of truth — TypeScript types are derived from it. This eliminates type/validation drift: if the schema changes, the type changes automatically. Works perfectly for API request validation and form data.
import { z } from 'zod';
// Define schema (runtime)
const UserSchema = z.object({
id: z.number().positive(),
name: z.string().min(1).max(100),
email: z.string().email(),
role: z.enum(['admin', 'user', 'moderator']),
age: z.number().min(18).optional()
});
// Derive TypeScript type — no duplication
type User = z.infer<typeof UserSchema>;
// { id: number; name: string; email: string; role: 'admin'|'user'|'moderator'; age?: number }
// Validate API request body
app.post('/users', async (req, res) => {
const result = UserSchema.safeParse(req.body);
if (!result.success) return res.status(400).json(result.error.flatten());
const user = result.data; // fully typed as User
await db.users.create({ data: user });
});Declaration files (.d.ts) contain only type information — no runtime code. They describe the public API of a JavaScript module so TypeScript can type-check callers. Library authors ship them alongside their JS, or they’re published separately under @types/*.
// math-utils.d.ts — describes math-utils.js without shipping TS source
declare module 'math-utils' {
export function add(a: number, b: number): number;
export function subtract(a: number, b: number): number;
export function clamp(value: number, min: number, max: number): number;
export const PI: number;
export type Radians = number;
}
// Generate .d.ts from source automatically in tsconfig.json
// { "declaration": true, "declarationDir": "./types" }
// Inline declaration in same dir (ambient module)
// types/legacy-lib.d.ts
declare module 'legacy-lib' {
function doStuff(x: string): boolean;
export = doStuff; // CommonJS default export style
}DefinitelyTyped hosts community-maintained .d.ts files for packages that don’t ship their own types. Install the types package as a dev dependency. If no @types exists and you don’t want to write full declarations, a stub declaration silences the error.
# Install types for Express
npm install --save-dev @types/express @types/node @types/cors
# Check if a package has types (bundled or @types)
npx arethetypeswrong --pack some-package
# Write a stub when no @types exists — types/some-package.d.ts
# declare module 'some-package'; ← treats all imports as 'any' — silences error
# OR provide partial types:
# declare module 'some-package' {
# export function doThing(x: string): number;
# }
# tsconfig.json — tell TypeScript where to find custom type declarations
# { "compilerOptions": { "typeRoots": ["./types", "./node_modules/@types"] } }Module augmentation re-opens a module’s type declarations and adds new types without forking the library. Used to add custom properties to Express’s Request, extend ProcessEnv for typed env vars, or augment global interfaces like Window.
// src/types/express.d.ts — augment Express Request
import 'express';
declare module 'express' {
interface Request {
user?: { id: number; role: string };
requestId?: string;
}
}
// src/types/env.d.ts — typed environment variables
declare global {
namespace NodeJS {
interface ProcessEnv {
NODE_ENV: 'development' | 'production' | 'test';
DATABASE_URL: string;
JWT_SECRET: string;
PORT?: string;
}
}
}
// Now process.env.DATABASE_URL is string (not string | undefined)
// And process.env.UNKNOWN gives a type errorPath aliases map a short prefix to a directory, turning ../../../utils/logger into @/utils/logger. TypeScript resolves them for type checking; your bundler (Vite, webpack, ts-node) must also be configured to resolve them at runtime.
// tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/components/*"],
"@utils/*": ["src/utils/*"],
"@types/*": ["src/types/*"]
}
}
}
// vite.config.ts — mirror the paths for bundler resolution
// resolve: { alias: { '@': path.resolve(__dirname, 'src') } }
// Usage — clean imports regardless of nesting depth
import { UserService } from '@/services/UserService';
import { Button } from '@components/Button';
import { formatCurrency }from '@utils/format';"isolatedModules": true errors if you write patterns that require cross-file type information to erase — because tools like Babel and esbuild transpile each file in isolation (no full type-checking). Required when using Vite, Babel, or SWC to transpile TypeScript.
// ❌ Re-exporting a type without 'type' keyword — Babel can't know it's a type
export { User } from './user'; // is User a type or value? Babel doesn't know
// ✅ Explicit type-only export — erasable by any transpiler
export type { User } from './user';
// ❌ const enum — requires cross-file info to inline
const enum Direction { Up, Down } // isolatedModules error
// ✅ Use regular enum or union literals instead
enum Direction { Up = 'UP', Down = 'DOWN' }
type Direction = 'UP' | 'DOWN';
// ❌ Namespace (legacy module format) causes issues
// ✅ Use ES modules instead
// tsconfig.json
// { "isolatedModules": true, "verbatimModuleSyntax": true }Project references let you split a codebase into smaller TypeScript projects that reference each other. tsc --build only recompiles projects whose inputs changed. Each sub-project has its own tsconfig.json with "composite": true.
// packages/shared/tsconfig.json
{ "compilerOptions": { "composite": true, "outDir": "./dist", "declaration": true } }
// packages/api/tsconfig.json
{
"compilerOptions": { "composite": true, "outDir": "./dist" },
"references": [{ "path": "../shared" }]
}
// Root tsconfig.json — orchestrates all packages
{
"files": [],
"references": [
{ "path": "packages/shared" },
{ "path": "packages/api" },
{ "path": "packages/web" }
]
}
// Build commands
// tsc --build ← incremental build (only changed packages)
// tsc --build --clean ← clean all output
// tsc --build --watch ← watch mode across all packagesType-only imports/exports tell TypeScript (and transpilers) that the import exists only for type checking — it will be completely erased from the output. This avoids circular dependency issues and ensures bundlers don’t accidentally include type-only modules in the output.
// import type — erased from compiled output
import type { User, CreateUserDto } from './user.types';
import type { Request, Response } from 'express';
// Mix value and type imports
import { UserService } from './UserService'; // value — kept
import type { UserDto } from './UserService'; // type — erased
// export type — explicit re-export of types
export type { User, Admin } from './models';
export { UserService } from './services'; // value re-export
// verbatimModuleSyntax (TS 5.0+) — replaces isolatedModules for this concern
// Forces all type imports to use 'import type' syntax
// tsconfig: { "verbatimModuleSyntax": true }Ambient declarations use declare to tell TypeScript about values that exist in the runtime environment but aren’t imported — globals from <script> tags, browser extensions, or environment-injected variables. They only affect types, not output.
// globals.d.ts — describe values injected by build tool or CDN
declare const __APP_VERSION__: string; // injected by Vite define
declare const __BUILD_DATE__: string;
declare const google: typeof import('@types/google.maps'); // loaded via CDN script
// Use without import
console.log(__APP_VERSION__); // ✅ TypeScript knows this exists
// Ambient module for non-JS assets (CSS modules, SVG, etc.)
declare module '*.svg' {
const content: string;
export default content;
}
declare module '*.module.css' {
const styles: Record<string, string>;
export default styles;
}
// Now you can import SVGs/CSS modules without type errors
import logo from './logo.svg';
import styles from './App.module.css';Define props as an interface or type alias and pass it as the function parameter type. Prefer plain function components over React.FC — the latter adds an implicit children prop and wraps the return in ReactElement | null which can hide type errors.
interface ButtonProps {
label: string;
variant?: 'primary' | 'secondary' | 'danger';
disabled?: boolean;
onClick: () => void;
icon?: React.ReactNode;
}
// Plain function — preferred over React.FC
function Button({ label, variant = 'primary', disabled = false, onClick, icon }: ButtonProps) {
return (
<button
className={`btn btn-${variant}`}
disabled={disabled}
onClick={onClick}
>
{icon} {label}
</button>
);
}
// Generic component
interface ListProps<T> {
items: T[];
renderItem: (item: T, index: number) => React.ReactNode;
keyExtractor: (item: T) => string | number;
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return <ul>{items.map((item, i) => <li key={keyExtractor(item)}>{renderItem(item, i)}</li>)}</ul>;
}Hooks use generics. useState<T> types the state; if inference fails, provide the generic explicitly. useRef<T> types the ref value — use null as initial value for DOM refs. useReducer infers types from the reducer function signature.
// useState — explicit generic when inference isn't enough
const [user, setUser] = React.useState<User | null>(null);
const [errors, setErrors] = React.useState<Record<string, string>>({});
const [status, setStatus] = React.useState<'idle'|'loading'|'done'|'error'>('idle');
// useRef — DOM ref needs the exact element type
const inputRef = React.useRef<HTMLInputElement>(null);
inputRef.current?.focus(); // HTMLInputElement | null
// Mutable ref (stores a non-DOM value)
const timerRef = React.useRef<ReturnType<typeof setTimeout>>();
// useReducer — infer types from reducer
type State = { count: number; step: number };
type Action = { type: 'increment' } | { type: 'setStep'; step: number } | { type: 'reset' };
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'increment': return { ...state, count: state.count + state.step };
case 'setStep': return { ...state, step: action.step };
case 'reset': return { count: 0, step: 1 };
}
}
const [state, dispatch] = React.useReducer(reducer, { count: 0, step: 1 });React provides typed event handler types: React.ChangeEventHandler<T>, React.FormEventHandler<T>, React.MouseEventHandler<T>, etc. Use them to type handler variables, or inline the event type directly in the function parameter.
// Inline — most common
function Form() {
const [value, setValue] = React.useState('');
return (
<input
value={value}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setValue(e.target.value)}
/>
);
}
// Named handler with explicit type
const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
setValue(e.target.value); // e is fully typed
};
const handleSubmit: React.FormEventHandler<HTMLFormElement> = (e) => {
e.preventDefault();
// ...
};
const handleClick: React.MouseEventHandler<HTMLButtonElement> = (e) => {
e.currentTarget.disabled = true;
};
// Select element
const handleSelect: React.ChangeEventHandler<HTMLSelectElement> = (e) => {
setRole(e.target.value as 'admin' | 'user');
};React.ComponentProps<T> extracts the props type of a component or HTML element. Use it to extend existing component APIs without importing their prop types directly — reducing coupling and staying in sync automatically.
// Extend native input with custom props
type InputProps = React.ComponentProps<'input'> & {
label: string;
error?: string;
};
function Input({ label, error, ...inputProps }: InputProps) {
return (
<div>
<label>{label}</label>
<input {...inputProps} className={error ? 'input-error' : ''} />
{error && <span className="error">{error}</span>}
</div>
);
}
// Extend a custom component
type IconButtonProps = React.ComponentProps<typeof Button> & {
icon: React.ReactNode;
};
// ComponentPropsWithoutRef — excludes ref (for non-forwardRef components)
// ComponentPropsWithRef — includes ref type
type NativeButton = React.ComponentPropsWithRef<'button'>;The default context value is often undefined when no provider is present. Wrapping useContext in a typed custom hook that throws if the context is missing eliminates the != null check at every call site and gives a helpful error message when misused.
interface ThemeContextValue { theme: 'light' | 'dark'; toggleTheme(): void; }
const ThemeContext = React.createContext<ThemeContextValue | undefined>(undefined);
// Custom hook — throws if used outside provider
function useTheme(): ThemeContextValue {
const ctx = React.useContext(ThemeContext);
if (ctx === undefined) throw new Error('useTheme must be inside <ThemeProvider>');
return ctx; // ThemeContextValue — undefined eliminated
}
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = React.useState<'light' | 'dark'>('light');
return (
<ThemeContext.Provider value={{ theme, toggleTheme: () => setTheme(t => t === 'light' ? 'dark' : 'light') }}>
{children}
</ThemeContext.Provider>
);
}
// Usage — no null check needed
function NavBar() {
const { theme, toggleTheme } = useTheme();
return <button onClick={toggleTheme}>{theme}</button>;
}Custom hooks infer their return type from the implementation. For tuple returns, TypeScript may infer a wider array type — add as const or an explicit return type annotation to get a true tuple. For object returns, inference works naturally.
// Tuple return — needs explicit type annotation
function useToggle(initial = false): [boolean, () => void, (v: boolean) => void] {
const [value, setValue] = React.useState(initial);
const toggle = React.useCallback(() => setValue(v => !v), []);
return [value, toggle, setValue];
// Without annotation, TypeScript infers: (boolean | (() => void) | ((v: boolean) => void))[]
}
const [open, toggle, setOpen] = useToggle();
// open: boolean, toggle: () => void — correct types
// Object return — inference works fine
function usePagination(pageSize: number) {
const [page, setPage] = React.useState(1);
return {
page,
pageSize,
nextPage: () => setPage(p => p + 1),
prevPage: () => setPage(p => Math.max(1, p - 1)),
reset: () => setPage(1)
}; // TypeScript infers the object type correctly
}
type PaginationReturn = ReturnType<typeof usePagination>;React.forwardRef is generic: forwardRef<RefType, PropsType>. The first generic is the type of the ref (usually an HTML element), the second is the component’s props. TypeScript ensures callers pass the right ref type.
interface TextInputProps {
label: string;
placeholder?: string;
error?: string;
}
// forwardRef<RefType, PropsType>
const TextInput = React.forwardRef<HTMLInputElement, TextInputProps>(
function TextInput({ label, placeholder, error }, ref) {
return (
<div>
<label>{label}</label>
<input ref={ref} placeholder={placeholder} />
{error && <p className="error">{error}</p>}
</div>
);
}
);
// Usage — ref is typed as React.RefObject<HTMLInputElement>
const inputRef = React.useRef<HTMLInputElement>(null);
<TextInput ref={inputRef} label="Name" />
inputRef.current?.focus(); // HTMLInputElement — fully typedThe right children type depends on what your component accepts. React.ReactNode is the most permissive (includes null, arrays, strings). React.ReactElement requires a JSX element. A function-as-children (render prop) pattern needs its own function signature.
// Most permissive — use for layout/wrapper components
interface LayoutProps { children: React.ReactNode }
// Requires a single ReactElement (no strings/nulls)
interface CloneProps { children: React.ReactElement }
// Exactly one element with specific type (rare)
interface TabsProps { children: React.ReactElement<TabProps> | React.ReactElement<TabProps>[] }
// Function as children (render prop)
interface DataProvider<T> {
children: (data: T, isLoading: boolean) => React.ReactNode;
}
function UserDataProvider({ children }: DataProvider<User>) {
const [user, setUser] = React.useState<User | null>(null);
return <>{children(user!, false)}</>;
}
// Usage
<UserDataProvider>
{(user, isLoading) => isLoading ? <Spinner /> : <Profile user={user} />}
</UserDataProvider>Express’s Request generic accepts four type parameters: Request<Params, ResBody, ReqBody, Query>. Providing them gives fully typed access to req.params, req.body, and req.query within the handler.
import { Request, Response, Router } from 'express';
interface OrderParams { orderId: string }
interface OrderQuery { include?: string }
interface UpdateBody { status: 'pending' | 'shipped' | 'delivered'; note?: string }
const router = Router();
router.get<OrderParams, unknown, unknown, OrderQuery>(
'/orders/:orderId',
async (req, res) => {
req.params.orderId; // string — typed
req.query.include; // string | undefined — typed
// ...
}
);
router.patch<OrderParams, unknown, UpdateBody>(
'/orders/:orderId',
async (req, res) => {
req.body.status; // 'pending' | 'shipped' | 'delivered'
req.body.note; // string | undefined
// Note: body type is trusting — validate at runtime with Zod
}
);Prisma reads the schema.prisma file and generates a typed client where every model, query, and result is fully typed. No manual type definitions needed. prisma generate regenerates types after schema changes — keeping types and DB schema in sync.
import { PrismaClient, Prisma } from '@prisma/client';
const prisma = new PrismaClient();
// Return type inferred from query shape
const user = await prisma.user.findUnique({
where: { id: 1 },
select: { id: true, name: true, email: true }
});
// user: { id: number; name: string; email: string } | null — inferred!
// Utility types for function signatures
type UserWithOrders = Prisma.UserGetPayload<{
include: { orders: { include: { items: true } } }
}>;
// Create input type — all fields validated by Prisma
async function createUser(data: Prisma.UserCreateInput) {
return prisma.user.create({ data });
}
// Update input — all fields optional
async function patchUser(id: number, data: Prisma.UserUpdateInput) {
return prisma.user.update({ where: { id }, data });
}Share types between the server and client in a shared package. The client’s fetch wrapper uses generics to return the correct type for each endpoint. Tools like tRPC or Hono take this further with end-to-end type inference.
// packages/shared/types.ts — shared between server and client
export interface User { id: number; name: string; email: string }
export interface Order { id: number; userId: number; total: number; status: string }
export interface ApiError{ message: string; code: string }
// packages/client/api.ts — typed API client
async function get<T>(path: string): Promise<T> {
const res = await fetch(`${API_BASE}${path}`, { headers: authHeaders() });
if (!res.ok) { const e: ApiError = await res.json(); throw e; }
return res.json() as Promise<T>;
}
// Specific typed methods
export const api = {
users: {
list: () => get<User[]>('/users'),
get: (id: number) => get<User>(`/users/${id}`),
},
orders: {
list: () => get<Order[]>('/orders'),
get: (id: number) => get<Order>(`/orders/${id}`),
}
};
const users = await api.users.list(); // User[]TypeScript treats all process.env.* as string | undefined. A typed env validation module (using Zod or manual checks) reads env vars at startup, validates them, and exports a typed object. Prevents crashes from missing env vars at the call site rather than at startup.
import { z } from 'zod';
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']),
PORT: z.string().default('3000').transform(Number),
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
REDIS_URL: z.string().url().optional()
});
// Parse at app startup — throws if anything is missing/invalid
const env = envSchema.parse(process.env);
// env is fully typed: { NODE_ENV: 'development'|'production'|'test'; PORT: number; ... }
export default env;
// Usage — no undefined checks needed
import env from './env';
const server = app.listen(env.PORT); // number
const client = new PrismaClient({ datasources: { db: { url: env.DATABASE_URL } } });Node’s built-in EventEmitter is untyped by default. A typed event map approach constrains which event names can be emitted and what payload each event carries — catching mistyped event names and wrong payload shapes at compile time.
import EventEmitter from 'node:events';
// Typed event map
interface OrderEvents {
created: [order: { id: number; total: number }];
shipped: [orderId: number; trackingCode: string];
cancelled: [orderId: number; reason: string];
}
// Typed emitter using declaration merging
declare interface OrderEmitter {
on<K extends keyof OrderEvents>(event: K, listener: (...args: OrderEvents[K]) => void): this;
emit<K extends keyof OrderEvents>(event: K, ...args: OrderEvents[K]): boolean;
}
class OrderEmitter extends EventEmitter {}
const emitter = new OrderEmitter();
emitter.on('created', (order) => console.log(order.id)); // order is typed!
emitter.emit('created', { id: 1, total: 99.99 }); // ✅
emitter.emit('created', { id: 1 }); // ❌ missing totalAsync generators have the type AsyncGenerator<YieldType, ReturnType, NextType>. Use them for paginating APIs, streaming data, or producing values lazily. Enable "downlevelIteration": true and target ES2018+ for full support.
// Typed async generator — paginates an API
async function* fetchAllPages<T>(url: string): AsyncGenerator<T[], void, undefined> {
let cursor: string | undefined;
do {
const endpoint = cursor ? `${url}?cursor=${cursor}` : url;
const res: { items: T[]; nextCursor?: string } = await fetch(endpoint).then(r => r.json());
yield res.items;
cursor = res.nextCursor;
} while (cursor);
}
// Consume with for-await-of
for await (const page of fetchAllPages<Order>('/api/orders')) {
processOrders(page); // page: Order[]
}
// Collect all
async function fetchAll<T>(url: string): Promise<T[]> {
const all: T[] = [];
for await (const page of fetchAllPages<T>(url)) all.push(...page);
return all;
}Plain TypeScript DI uses constructor injection with interfaces. The interface decouples consumers from implementations — swap the real service for a test double by passing a different object that satisfies the interface. No IoC container needed for most apps.
// Interfaces define the contracts
interface IUserRepo { findById(id: number): Promise<User | null>; save(u: User): Promise<User>; }
interface IMailer { send(to: string, subject: string, body: string): Promise<void>; }
interface ILogger { info(msg: string): void; error(msg: string, err?: Error): void; }
class UserService {
constructor(
private readonly repo: IUserRepo,
private readonly mailer: IMailer,
private readonly logger: ILogger
) {}
async register(data: CreateUserDto): Promise<User> {
const user = await this.repo.save({ ...data, id: 0 });
await this.mailer.send(user.email, 'Welcome!', `Hi ${user.name}`);
this.logger.info(`User ${user.id} registered`);
return user;
}
}
// Test double satisfies the interface
const mockMailer: IMailer = { send: jest.fn().mockResolvedValue(undefined) };
const service = new UserService(realRepo, mockMailer, realLogger);The Web Streams API (ReadableStream, TransformStream, WritableStream) is typed in lib.dom.d.ts. TypeScript generics carry the chunk type through the pipeline. Available in modern Node.js (18+) and all browsers.
// Streaming JSON response in Next.js / Hono / Deno
function createStream(): ReadableStream<string> {
const encoder = new TextEncoder();
return new ReadableStream<string>({
async start(controller: ReadableStreamDefaultController<string>) {
const rows = db.users.cursor(); // async iterable
for await (const row of rows) {
controller.enqueue(JSON.stringify(row) + '\n');
}
controller.close();
}
});
}
// Transform stream — parse NDJSON chunks
const parseJSON = new TransformStream<string, unknown>({
transform(chunk, controller) {
chunk.split('\n').filter(Boolean).forEach(line => {
controller.enqueue(JSON.parse(line));
});
}
});
const stream: ReadableStream<unknown> = createStream().pipeThrough(parseJSON);typescript-eslint provides ESLint rules that understand TypeScript types — not just syntax. Rules like no-floating-promises, await-thenable, no-unsafe-assignment, and strict-boolean-expressions catch semantic bugs the compiler doesn’t.
// eslint.config.mjs (flat config)
import tseslint from 'typescript-eslint';
export default tseslint.config(
tseslint.configs.recommendedTypeChecked,
{
languageOptions: {
parserOptions: { project: true, tsconfigRootDir: import.meta.dirname }
},
rules: {
'@typescript-eslint/no-floating-promises': 'error', // missing await
'@typescript-eslint/no-unsafe-assignment': 'warn',
'@typescript-eslint/strict-boolean-expressions': 'error', // no truthy coercion
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/consistent-type-imports': 'error', // enforce import type
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }]
}
}
);ts-node transpiles TypeScript on the fly via tsc (with type checking). tsx uses esbuild — 10× faster but no type checking. Use ts-node when you need type errors in dev scripts; tsx when speed matters more (test runners, CLI tools).
# ts-node — type-checked execution
npm install --save-dev ts-node
npx ts-node src/scripts/seed.ts
npx ts-node --esm src/server.ts # ESM mode
# tsx — fast, no type checking
npm install --save-dev tsx
npx tsx src/scripts/seed.ts
npx tsx watch src/server.ts # watch mode
# package.json scripts
{
"scripts": {
"dev": "tsx watch src/index.ts",
"seed": "tsx src/scripts/seed.ts",
"build": "tsc",
"typecheck": "tsc --noEmit"
}
}
# Run with Node 22.6+ — built-in TS support (strips types)
node --experimental-strip-types src/index.tsPrefixing a type parameter with const in a generic function makes TypeScript infer the narrowest possible type from the argument — as if as const were applied. Avoids the need for callers to add as const at every call site.
// Without const type parameter — T inferred as string[]
function makeArray<T>(items: T[]): T[] { return items; }
const a = makeArray(['a', 'b', 'c']); // string[]
// With const type parameter (TS 5.0+) — T inferred as literal tuple
function makeArray<const T extends readonly unknown[]>(items: T): T { return items; }
const b = makeArray(['a', 'b', 'c'] as const); // ['a', 'b', 'c'] — preserved
// Practical: typed route registration
function createRoute<const Method extends string, const Path extends string>(
method: Method, path: Path
) { return { method, path } as { method: Method; path: Path }; }
const route = createRoute('GET', '/users/:id');
// { method: 'GET'; path: '/users/:id' } — literals preserved!TypeScript 5.2 added the using declaration for the Explicit Resource Management proposal. When a variable declared with using goes out of scope, its [Symbol.dispose]() method is called automatically — similar to C# using or Rust Drop. await using calls [Symbol.asyncDispose]().
// A disposable resource
class DatabaseTransaction {
private committed = false;
constructor(private conn: Connection) {}
async commit() { await this.conn.commit(); this.committed = true; }
[Symbol.dispose]() {
if (!this.committed) this.conn.rollback(); // auto-rollback on scope exit
this.conn.release();
}
}
async function transfer(fromId: number, toId: number, amount: number) {
using tx = new DatabaseTransaction(await pool.connect());
// tx is released (and rolled back if not committed) when function exits
// — even if an exception is thrown
await db.debit(fromId, amount, tx);
await db.credit(toId, amount, tx);
await tx.commit();
} // ← tx[Symbol.dispose]() called automatically hereWith --noEmit, tsc runs a full type check but produces no output files. This is the fast path for CI: validate types without waiting for a production build. Run in parallel with tests for faster feedback.
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '22', cache: 'npm' }
- run: npm ci
# Run type-check and tests in parallel
- name: Type check
run: npx tsc --noEmit
- name: Lint
run: npx eslint .
- name: Tests
run: npm test -- --coverage
# Build only after all checks pass
- name: Build
run: npm run buildA typed feature flag system uses a union of known flag names as the key. TypeScript catches references to undefined flags at compile time — preventing typos that would silently return undefined at runtime.
// Define all flags as a union
type FeatureFlag =
| 'new-checkout-flow'
| 'ai-recommendations'
| 'dark-mode'
| 'multi-currency';
type FlagConfig = { enabled: boolean; rollout?: number; description: string };
type FlagMap = Record<FeatureFlag, FlagConfig>;
const flags: FlagMap = {
'new-checkout-flow': { enabled: true, rollout: 50, description: 'New checkout experience' },
'ai-recommendations': { enabled: false, description: 'AI-powered product recs' },
'dark-mode': { enabled: true, description: 'Dark mode toggle' },
'multi-currency': { enabled: false, description: 'Multi-currency support' }
};
function isEnabled(flag: FeatureFlag): boolean {
return flags[flag].enabled;
}
isEnabled('dark-mode'); // ✅
isEnabled('new-checkout-flow'); // ✅
isEnabled('beta-feature'); // ❌ not a valid FeatureFlagstructuredClone<T> infers the return type from the input. Object.fromEntries has limited inference — it returns Record<string, V> rather than a precise object type. Explicit generics or as casts are sometimes needed to regain type precision.
// structuredClone — preserves type
const user: User = { id: 1, name: 'Alice', email: 'alice@example.com' };
const clone = structuredClone(user); // type: User ✅
// Object.fromEntries — loses key specificity
const entries: [string, number][] = [['a', 1], ['b', 2]];
const obj1 = Object.fromEntries(entries);
// { [x: string]: number } — not { a: number; b: number }
// Typed fromEntries helper
type FromEntries<T extends readonly [PropertyKey, unknown][]> = {
[K in T[number] as K[0]]: K[1]
};
function fromEntries<T extends readonly [PropertyKey, unknown][]>(entries: T): FromEntries<T> {
return Object.fromEntries(entries) as FromEntries<T>;
}
const typed = fromEntries([['a', 1], ['b', 'x']] as const);
typed.a; // number
typed.b; // stringWhen facing a complex type error, extract intermediate types into named aliases and hover over them in your IDE. The Prettify trick flattens intersection types into a readable object. satisfies can check a type without widening.
// Prettify — expand intersections into a single readable object
type Prettify<T> = { [K in keyof T]: T[K] } & {};
type A = { id: number } & { name: string } & { role: 'admin' };
type B = Prettify<A>; // hover shows: { id: number; name: string; role: 'admin' }
// Check what TypeScript infers for a complex expression
type InferredConfig = Prettify<ReturnType<typeof createConfig>>>;
// hover over InferredConfig to see the full shape
// Use declare to test types without runtime values
declare const user: Awaited<ReturnType<typeof fetchUser>>;
user.name; // check IntelliSense — is name string?
// Trap: type errors in complex generics — simplify step by step
// If T<A><B><C> fails, check T<A> first, then T<A><B>, etc.
type Step1 = SomeComplexType<User>;
type Step2 = Step1 extends object ? keyof Step1 : never;TypeScript’s control flow narrows the type in each case block based on the discriminant. The default branch receives never if all cases are handled — used to enforce exhaustiveness. A helper function makes this check reusable.
type Action =
| { type: 'ADD_ITEM'; item: CartItem }
| { type: 'REMOVE_ITEM'; id: number }
| { type: 'SET_QTY'; id: number; qty: number }
| { type: 'CLEAR' };
function assertNever(x: never, msg = 'Unexpected value'): never {
throw new Error(`${msg}: ${JSON.stringify(x)}`);
}
function cartReducer(state: CartState, action: Action): CartState {
switch (action.type) {
case 'ADD_ITEM': return { ...state, items: [...state.items, action.item] };
case 'REMOVE_ITEM': return { ...state, items: state.items.filter(i => i.id !== action.id) };
case 'SET_QTY': return { ...state, items: state.items.map(i => i.id === action.id ? { ...i, qty: action.qty } : i) };
case 'CLEAR': return { ...state, items: [] };
default: return assertNever(action); // compile error if new action type not handled
}
}Omit<A | B, 'key'> doesn’t distribute over unions — it evaluates keyof (A | B) first (only shared keys), then omits from that. This loses the union structure. The fix is a distributive DistributiveOmit type.
type A = { type: 'a'; x: number; id: string };
type B = { type: 'b'; y: string; id: string };
type Union = A | B;
// ❌ Omit doesn't distribute — result loses union structure
type Wrong = Omit<Union, 'id'>;
// { type: 'a' | 'b' } — lost x and y!
// ✅ Distributive omit — applies Omit to each union member separately
type DistributiveOmit<T, K extends PropertyKey> = T extends any ? Omit<T, K> : never;
type Correct = DistributiveOmit<Union, 'id'>;
// { type: 'a'; x: number } | { type: 'b'; y: string } — correct!
// Same pattern for Pick, Partial, Required
type DistributivePick<T, K extends keyof T> = T extends any ? Pick<T, K> : never;
type DistributivePartial<T> = T extends any ? Partial<T> : never;Type-level tests use conditional types with never to assert that two types are equal. Libraries like tsd or expect-type provide readable assertion APIs. Run them as part of tsc --noEmit in CI.
// Manual type equality assertion
type Equal<A, B> = [A] extends [B] ? [B] extends [A] ? true : false : false;
type Expect<T extends true> = T; // compile error if T is false
// Tests — these are compile-time assertions, no runtime code
type Tests = [
Expect<Equal<Exclude<'a' | 'b' | 'c', 'a'>, 'b' | 'c'>>,
Expect<Equal<ReturnType<() => string>, string>>,
Expect<Equal<Awaited<Promise<number>>, number>>,
Expect<Equal<Partial<{ a: 1; b: 2 }>, { a?: 1; b?: 2 }>>
];
// With expect-type library
import { expectType, expectError } from 'tsd';
expectType<string>(identity('hello'));
expectError(identity(42) as never); // ensure this would be an errorTypeScript 5.5 introduced inferred type predicates: when a function’s return expression is a type-narrowing condition, TypeScript automatically infers an x is T return type. This means array.filter(x => x !== null) now correctly infers NonNullable<T>[] without a manual type predicate.
// Before TS 5.5 — filter returned T[] not NonNullable<T>[]
const items: (string | null)[] = ['a', null, 'b', null, 'c'];
const withNull = items.filter(x => x !== null); // (string | null)[] — not narrowed!
const withPred = items.filter((x): x is string => x !== null); // string[] ✅
// TS 5.5+ — inferred type predicates narrow filter automatically
const withNull55 = items.filter(x => x !== null); // string[] ✅ — inferred!
// Works with instanceof
class Cat { meow() {} }
class Dog { bark() {} }
const pets: (Cat | Dog | null)[] = [new Cat(), null, new Dog()];
const cats = pets.filter(p => p instanceof Cat); // Cat[] — auto-inferred! ✅
// Works with custom guards that TS 5.5 can infer
const users = userOrNulls.filter(u => u !== null && u.active); // User[] narrowed