Skip to main content

TypeScript Interview Questions (2026)

100 real interview questions with in-depth answers — 30 basic, 40 intermediate, 30 advanced. Updated April 2026.

Preparing for a TypeScript Developer role?

TypeScript is a statically typed superset of JavaScript developed by Microsoft. Every valid JavaScript file is also a valid TypeScript file; TypeScript simply adds optional type annotations, interfaces, and compile-time checks that are stripped away before the code runs. The TypeScript compiler (`tsc`) transpiles `.ts` files to plain `.js`, letting you catch type errors during development rather than at runtime. Because it compiles to JavaScript, TypeScript runs anywhere JavaScript does — browsers, Node.js, Deno, etc.

You add a colon followed by the type after a variable name. TypeScript also infers types from initializers, so explicit annotations are often optional.

ts
let name: string = "Alice";
let age: number = 30;
let active: boolean = true;
let wildcard: any = 42;       // opt-out of type checking
let safe: unknown = "hello";  // must narrow before use
function loop(): never { while (true) {} }  // never returns
function log(): void { console.log("hi"); } // returns undefined

Prefer `unknown` over `any` because it forces explicit narrowing before operations are allowed.

`any` completely disables type checking for a value — you can call methods, index into it, or pass it anywhere without errors. `unknown` is the type-safe counterpart: you can assign anything to `unknown`, but you cannot operate on it until you narrow the type with a type guard. This makes `unknown` far safer for representing values whose types you genuinely do not know (e.g., parsed JSON, external API responses).

ts
let a: any = "hello";
a.toFixed(); // no error — dangerous

let u: unknown = "hello";
// u.toFixed(); // Error: Object is of type unknown
if (typeof u === "string") u.toUpperCase(); // OK after narrowing

A `type` alias creates a name for any type expression — primitives, unions, intersections, tuples, object shapes, and even complex conditional types. Unlike interfaces, type aliases can alias union types and other non-object types. They cannot be re-opened (extended after declaration) in the same way interfaces can.

ts
type ID = string | number;
type Point = { x: number; y: number };
type Callback<T> = (value: T) => void;

Use `type` when you need unions, intersections, or mapped types; use `interface` for object shapes that may need extending.

An `interface` declares the shape of an object — its properties, their types, and optional or readonly modifiers. Interfaces support inheritance via `extends` and can be merged across multiple declarations (declaration merging). They are erased at compile time and produce no runtime artifact.

ts
interface User {
  id: number;
  name: string;
  email?: string;
}

interface Admin extends User {
  role: "admin";
}

Interfaces are preferred for public API contracts and library typings because they are extendable and produce cleaner error messages.

The most important differences: (1) `interface` supports declaration merging — you can declare the same interface name twice and TypeScript merges them; `type` aliases cannot be re-declared. (2) `type` aliases can represent any type including unions, tuples, and primitives, while `interface` can only describe object shapes. (3) Both support generics and `extends`, but `type` uses intersection (`&`) while `interface` uses `extends`. (4) Class `implements` works with both. Error messages with `type` sometimes show the full expanded shape whereas `interface` names appear inline. ```ts interface Foo { a: string } interface Foo { b: number } // merged: { a: string; b: number } type Bar = { a: string } // cannot re-declare ```
A union type expresses that a value can be one of several types, written with the `|` operator. You must narrow the type before calling type-specific methods. ```ts function format(value: string | number): string { if (typeof value === "number") return value.toFixed(2); return value.trim(); } ``` Union types are especially powerful with literal types and discriminated unions, enabling exhaustive pattern matching at compile time.
An intersection type (`&`) combines multiple types so that the resulting type has all properties from every constituent type. It is the dual of union types and is commonly used to merge object shapes. ```ts type HasName = { name: string }; type HasAge = { age: number }; type Person = HasName & HasAge; const p: Person = { name: "Alice", age: 30 }; // must satisfy both ``` Intersections of primitive types (e.g., `string & number`) produce `never` because no value can satisfy both simultaneously.
The `?` modifier on a property or parameter means the value may be `undefined` and TypeScript will not require it to be provided. The non-null assertion operator `!` is a postfix operator that tells TypeScript "I know this is not null or undefined" — it strips `null | undefined` from the type without any runtime check. ```ts interface Config { timeout?: number; // number | undefined } function process(cfg: Config) { // cfg.timeout could be undefined — must check const ms = cfg.timeout!; // assert it exists (use carefully) } ``` Overuse of `!` defeats the purpose of `strictNullChecks`; prefer explicit checks or default values.
A tuple is a fixed-length array where each element has a specific type at a known index. Unlike plain arrays, tuples enforce both the number of elements and their positional types. ```ts type RGB = [number, number, number]; const red: RGB = [255, 0, 0]; type Labeled = [x: number, y: number]; // labeled tuple (TS 4.0) const pt: Labeled = [10, 20]; ``` Tuples are useful for returning multiple values from a function, representing fixed-structure records, and modeling variadic rest arguments with known head/tail shapes.
An `enum` is a named set of constants. Numeric enums compile to a two-way mapping object (value ↔ name); string enums compile to a one-way object. ```ts enum Direction { Up, Down, Left, Right } // 0,1,2,3 // compiles to: { Up:0, 0:"Up", Down:1, 1:"Down", ... } const enum FastDir { Up, Down } // inlined at call sites, no object emitted ``` `const enum` values are inlined by the compiler and produce no runtime object, making them faster but incompatible with `isolatedModules` compilation (e.g., Babel, esbuild). Prefer union literal types (`"up" | "down"`) for simple string constants to avoid these pitfalls.
`readonly` marks a property or array as non-writable after initialization, enforced at compile time. `as const` is an assertion that tells TypeScript to infer the narrowest possible literal types and make all properties `readonly` deeply. ```ts const colors = ["red", "green", "blue"] as const; // type: readonly ["red", "green", "blue"] interface Point { readonly x: number; readonly y: number; } const p: Point = { x: 0, y: 0 }; // p.x = 1; // Error ``` `as const` is especially useful for object literals used as lookup tables or discriminated union factories because it prevents widening to `string`.
Type widening is what TypeScript does automatically when it infers a type from an initializer — for example, `let x = "hello"` widens to `string` rather than the literal `"hello"`. Type narrowing is the opposite: TypeScript refines a broad type into a narrower one inside a conditional block based on a type guard, equality check, or control flow. ```ts let x = "hello"; // widened to string const y = "hello"; // narrowed to literal "hello" function fn(val: string | number) { if (typeof val === "string") { val.toUpperCase(); // narrowed to string here } } ``` Control-flow analysis performs narrowing automatically across assignments, conditionals, and early returns.
A type guard is a runtime check that also narrows the type in the surrounding scope. Built-in type guards include `typeof` (for primitives), `instanceof` (for class instances), and `in` (for property presence). ```ts function handle(x: string | Error) { if (x instanceof Error) { console.log(x.message); // narrowed to Error } else { console.log(x.toUpperCase()); // narrowed to string } } function hasName(obj: unknown): obj is { name: string } { return typeof obj === "object" && obj !== null && "name" in obj; } ``` User-defined type guards use the `x is T` return type syntax to teach TypeScript about custom narrowing logic.
A literal type is a type that represents exactly one specific value rather than a broad set. Strings, numbers, and booleans all have literal variants. ```ts type Direction = "north" | "south" | "east" | "west"; type One = 1; type True = true; function move(dir: Direction) { /* ... */ } move("north"); // OK // move("up"); // Error: not assignable ``` Literal types combine with union types to form discriminated unions and exhaustive switch statements, making them one of the most powerful patterns in TypeScript.
`never` represents a value that never occurs. A function whose body always throws or loops infinitely has return type `never`. It is also the result of an impossible intersection (e.g., `string & number`) and is the bottom type — it is assignable to every type, but nothing is assignable to `never`. ```ts function fail(msg: string): never { throw new Error(msg); } type Impossible = string & number; // never ``` The most practical use is exhaustiveness checking: in the `default` branch of an exhaustive switch you assign the value to `never`, causing a compile error if a new union member is added without handling it.
`void` is the return type of a function that does not explicitly return a value (or returns `undefined` implicitly). `undefined` is a concrete type that represents the value `undefined`. A function typed as returning `void` is allowed to return `undefined`, but callers should not rely on its return value. ```ts function logMsg(): void { console.log("hi"); } // fine function getUndef(): undefined { return undefined; } // must return undefined const arr = [1, 2, 3]; arr.forEach((x): void => console.log(x)); // forEach callback typed void ``` The subtle difference matters in callback typing: `() => void` allows a callback to return anything, but that return value is ignored by the caller.
Function overloads let you declare multiple type signatures for a single function, giving callers a precise type depending on the arguments they pass. Only the last signature (the implementation signature) has a body and it must be compatible with all overload signatures. ```ts function parse(input: string): number; function parse(input: number): string; function parse(input: string | number): string | number { if (typeof input === "string") return parseInt(input, 10); return input.toString(); } const n = parse("42"); // inferred: number const s = parse(42); // inferred: string ``` Overloads produce more precise types than a single union signature but should be used sparingly — generics often express the same idea more cleanly.
A generic function uses a type parameter (conventionally `T`) that is determined at the call site, allowing the function to work with any type while still being type-safe. TypeScript usually infers the type parameter from the arguments. ```ts function identity<T>(value: T): T { return value; } const s = identity("hello"); // T inferred as string const n = identity<number>(42); // T explicitly provided ``` Generics enable reusable, composable utilities (like `Array.map`, `Promise`, `Result<T, E>`) without sacrificing type information.
`T extends U` constrains the type parameter `T` to only accept types that are assignable to `U`. This prevents callers from passing arbitrary types and lets you access properties of `U` on values of type `T` inside the function body. ```ts function getLength<T extends { length: number }>(val: T): number { return val.length; // safe: T is guaranteed to have length } getLength("hello"); // OK getLength([1, 2, 3]); // OK // getLength(42); // Error: number has no length ``` In conditional types, `T extends U ? X : Y` is a runtime-independent type-level conditional — it does not mean T inherits from U in the OOP sense.
`keyof T` produces a union of the string (or symbol) literal types of all known public keys of type `T`. It is foundational for building generic utilities that operate on object properties in a type-safe way. ```ts interface User { id: number; name: string; email: string } type UserKey = keyof User; // "id" | "name" | "email" function getField<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key]; // return type is T[K] — indexed access } ``` `keyof` combined with indexed access types (`T[K]`) allows you to build fully type-safe property pickers, reducers, and mappers.
At the value level, `typeof` is a JavaScript runtime operator. At the type level in TypeScript, `typeof x` captures the type of the variable or property `x` without you having to write it out. ```ts const config = { host: "localhost", port: 3000 }; type Config = typeof config; // { host: string; port: number } function greet(name: string) {} type GreetFn = typeof greet; // (name: string) => void ``` This is especially useful with `ReturnType<typeof fn>` and `InstanceType<typeof Class>` to derive types from runtime values rather than re-declaring them.
An index signature allows you to describe an object whose keys are not known at compile time but whose value types are consistent. The key type must be `string`, `number`, or `symbol`. ```ts interface StringMap { [key: string]: string; } const env: StringMap = {}; env["NODE_ENV"] = "production"; // OK // env["timeout"] = 30; // Error: number not assignable to string ``` When combining index signatures with known properties, all known property types must be compatible with the index signature value type, which is a common stumbling block.
TypeScript uses structural (duck) typing: two types are compatible if they have the same shape — the same set of properties with compatible types — regardless of their name or declaration origin. This is in contrast to nominal typing used in Java or C#, where two types are only compatible if they share a declared inheritance relationship. ```ts interface Point { x: number; y: number } class Vector { constructor(public x: number, public y: number) {} } const p: Point = new Vector(1, 2); // OK — same shape ``` This means you can satisfy interface contracts without explicitly implementing them, which makes TypeScript very flexible when working with third-party JavaScript.
With `strictNullChecks: false` (the default before strict mode), `null` and `undefined` are assignable to every type. With `strictNullChecks: true`, they become their own distinct types and you must explicitly include them in union types or handle them before use. ```ts // strictNullChecks: true let name: string = null; // Error let maybeNull: string | null = null; // OK function greet(name: string | null) { if (name === null) return "Hello, stranger"; return `Hello, ${name.toUpperCase()}`; // narrowed to string } ``` Enabling `strictNullChecks` (part of `strict: true`) is strongly recommended — it prevents a large class of null-reference bugs.
If you declare an interface with the same name multiple times in the same scope (or across modules in the global scope), TypeScript merges all declarations into a single interface. This is primarily used to augment library types without forking them. ```ts interface Window { myPlugin: () => void; // augment global Window } interface RequestInit { timeout?: number; // add property to fetch RequestInit } ``` Declaration merging only works with `interface`, not `type` aliases. It is the mechanism behind module augmentation patterns used in library typings.
`tsconfig.json` is the compiler configuration file for a TypeScript project. It specifies compiler options, include/exclude patterns, and project references. The `strict` flag is a shorthand that enables eight stricter checks simultaneously: `strictNullChecks`, `strictFunctionTypes`, `strictBindCallApply`, `strictPropertyInitialization`, `noImplicitAny`, `noImplicitThis`, `alwaysStrict`, and `useUnknownInCatchVariables`. ```json { "compilerOptions": { "target": "ES2022", "module": "ESNext", "strict": true, "outDir": "./dist" } } ``` Always start new projects with `strict: true` — retrofitting it onto a large codebase later is painful.
`.ts` files contain actual TypeScript source code that gets compiled to JavaScript. `.d.ts` files are declaration files — they contain only type information (no implementation) and are never compiled to runnable code. Declaration files describe the shape of existing JavaScript modules so TypeScript can type-check code that uses them. ```ts // math.d.ts export declare function add(a: number, b: number): number; export declare const PI: number; ``` When you publish an npm package you typically include `.d.ts` files alongside the compiled `.js` output (or set `declaration: true` in tsconfig to auto-generate them). The `@types/*` packages on npm are collections of `.d.ts` files for popular JavaScript libraries.
`import type` is a TypeScript-only import that is completely erased at compile time and never emits any JavaScript. It guarantees that the import is only used for type annotations, preventing accidental runtime dependency on a module. ```ts import type { User } from "./types"; // erased, no runtime import import { createUser } from "./users"; // value import — kept function process(user: User) { return createUser(user); } ``` With `verbatimModuleSyntax` (TS 5.0), using `import type` for type-only imports is enforced by the compiler. It also helps bundlers like esbuild and Babel (which cannot do type analysis) tree-shake correctly.
`noImplicitAny` causes TypeScript to raise an error whenever it would otherwise infer `any` for a variable or parameter because no type annotation or initializer is present. Without this flag, TypeScript silently falls back to `any`, undermining the entire purpose of using TypeScript. ```ts // noImplicitAny: true function greet(name) { // Error: name implicitly has type any return "Hello " + name; } function greet(name: string) { // OK — explicit annotation return "Hello " + name; } ``` `noImplicitAny` is included in the `strict` bundle. It forces developers to be explicit about types in function signatures, which is where most of the value of static typing lies.
TypeScript ships a library of generic utility types that transform existing types: ```ts type P = Partial<User>; // all props optional type R = Required<User>; // all props required type RO = Readonly<User>; // all props readonly type Rec = Record<string, number>; // { [k: string]: number } type Picked = Pick<User, "id" | "name">; type Omitted = Omit<User, "password">; type Ex = Exclude<"a" | "b" | "c", "a">; // "b" | "c" type Ext = Extract<"a" | "b" | number, string>; // "a" | "b" type NN = NonNullable<string | null | undefined>; // string type Ret = ReturnType<typeof fetch>; // Promise<Response> type Params = Parameters<typeof fetch>; // [input: ..., init?: ...] type Inst = InstanceType<typeof Date>; // Date ``` These are implemented using conditional types, mapped types, and `infer`. Understanding their implementations is essential for building custom utilities.
`Omit<T, K>` is defined as `Pick<T, Exclude<keyof T, K>>`. It first uses `keyof T` to get all property names, then `Exclude` removes the keys listed in `K`, and finally `Pick` selects the remaining keys from `T`. ```ts // Simplified implementation: type MyOmit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>; interface User { id: number; name: string; password: string } type SafeUser = MyOmit<User, "password">; // { id: number; name: string } ``` A subtle gotcha: the built-in `Omit` uses `keyof any` (i.e., `string | number | symbol`) rather than `keyof T` for the `K` constraint, meaning it accepts keys that do not actually exist on `T`. For stricter behavior use `Pick<T, Exclude<keyof T, K>>` with `K extends keyof T`.
Mapped types iterate over the keys of an existing type and produce a new type by transforming each property. The syntax is `{ [K in keyof T]: NewType }`. You can add or remove `readonly` and `?` modifiers with `+`/`-` prefixes. ```ts type Nullable<T> = { [K in keyof T]: T[K] | null }; type Mutable<T> = { -readonly [K in keyof T]: T[K] }; type AllRequired<T> = { [K in keyof T]-?: T[K] }; type Flags<T> = { [K in keyof T]: boolean }; type UserFlags = Flags<User>; // { id: boolean; name: boolean; email: boolean } ``` Mapped types underpin all the built-in utility types (`Partial`, `Required`, `Readonly`, `Record`). The `as` clause (TS 4.1) allows key remapping within the iteration: `[K in keyof T as Rename<K>]: ...`.
A conditional type uses the syntax `T extends U ? X : Y` — if `T` is assignable to `U`, the type resolves to `X`, otherwise to `Y`. They are evaluated lazily when the type parameter is known. ```ts type IsString<T> = T extends string ? true : false; type A = IsString<"hello">; // true type B = IsString<42>; // false type Flatten<T> = T extends Array<infer Item> ? Item : T; type F1 = Flatten<string[]>; // string type F2 = Flatten<number>; // number ``` Conditional types are the foundation for `Exclude`, `Extract`, `NonNullable`, `ReturnType`, and many advanced utilities. They also distribute over union types by default.
`infer` introduces a type variable inside the `extends` clause of a conditional type, letting you capture and name a portion of the matched type for use in the truthy branch. It is always used with conditional types — never alone. ```ts type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never; type UnpackPromise<T> = T extends Promise<infer V> ? V : T; type Head<T extends any[]> = T extends [infer H, ...any[]] ? H : never; type R = ReturnType<() => number>; // number type V = UnpackPromise<Promise<string>>; // string type H = Head<[boolean, string, number]>; // boolean ``` `infer` is most powerful when combined with tuple/rest patterns to extract positional types, making it the basis for `Parameters`, `ConstructorParameters`, and sophisticated template-literal parsers.
When the checked type in a conditional type is a naked type parameter (not wrapped in `[]` or another generic), the conditional type distributes over a union. TypeScript applies the condition to each union member separately and unions the results. ```ts type ToArray<T> = T extends any ? T[] : never; type R = ToArray<string | number>; // string[] | number[] // Non-distributive version: type ToArrayND<T> = [T] extends [any] ? T[] : never; type R2 = ToArrayND<string | number>; // (string | number)[] ``` Distribution is usually what you want for utility types like `Exclude` and `Extract`. Wrap `T` in a tuple `[T]` to prevent distribution when you need to treat the union as a whole.
Template literal types (introduced in TS 4.1) apply template literal syntax at the type level, enabling string type manipulation. They compose with union types to produce all permutations. ```ts type EventName = "click" | "focus" | "blur"; type Handler = `on${Capitalize<EventName>}`; // "onClick" | "onFocus" | "onBlur" type Getter<T extends string> = `get${Capitalize<T>}`; type G = Getter<"name" | "age">; // "getName" | "getAge" type Route = `/api/${string}`; const r: Route = "/api/users"; // OK ``` Template literal types power type-safe event handlers, CSS property names, URL path parsing, and string interpolation in type-level computations.
Recursive types reference themselves in their own definition, allowing you to model arbitrarily nested data structures like JSON, trees, and linked lists. TypeScript supports recursive type aliases since TS 3.7. ```ts type JSONValue = | string | number | boolean | null | JSONValue[] | { [key: string]: JSONValue }; type TreeNode<T> = { value: T; children?: TreeNode<T>[]; }; type NestedArray<T> = T | NestedArray<T>[]; ``` Recursive conditional types (e.g., `DeepPartial`) require TypeScript to detect cycles and may be deferred. TypeScript limits recursion depth to prevent infinite loops during compilation.
`DeepReadonly` recursively marks every property as `readonly`, including nested objects and arrays. It uses a conditional type to check whether a property value is an object, and if so, recurses into it. ```ts type DeepReadonly<T> = T extends (infer U)[] ? ReadonlyArray<DeepReadonly<U>> : T extends object ? { readonly [K in keyof T]: DeepReadonly<T[K]> } : T; type Config = DeepReadonly<{ db: { host: string; port: number }; flags: string[]; }>; // config.db.host is readonly, config.flags is ReadonlyArray ``` Handling arrays specially (via the `infer U` branch) is important because `{ readonly [K in keyof T] }` on an array only marks `length` and index signatures readonly, not the array methods.
`typeof` narrowing uses the JavaScript `typeof` operator inside an `if` or `switch` to tell TypeScript which primitive type a variable has in that branch. TypeScript tracks this through control-flow analysis and adjusts the type accordingly. ```ts function process(x: string | number | boolean) { if (typeof x === "string") { x.toUpperCase(); // string } else if (typeof x === "number") { x.toFixed(2); // number } else { x; // boolean — exhausted } } ``` TypeScript recognizes the seven `typeof` results: `"string"`, `"number"`, `"bigint"`, `"boolean"`, `"symbol"`, `"undefined"`, and `"object"` (which includes `null` — a known JS quirk). Always check `x !== null` separately when narrowing objects.
A discriminated union is a union of object types that share a common literal property (the discriminant). TypeScript uses the discriminant to narrow the union to a specific member inside conditionals. ```ts type Circle = { kind: "circle"; radius: number }; type Rectangle = { kind: "rectangle"; width: number; height: number }; type Shape = Circle | Rectangle; function area(s: Shape): number { switch (s.kind) { case "circle": return Math.PI * s.radius ** 2; case "rectangle": return s.width * s.height; } } ``` Discriminated unions are the TypeScript idiomatic way to model algebraic data types (like Rust enums or Haskell sum types). Adding a new variant causes a compile error in every exhaustive switch that does not handle it.
Add a `default` branch to an exhaustive `switch` that assigns the value to a variable typed as `never`. If all cases are handled, TypeScript infers the value as `never` in the default branch and the assignment succeeds. If a new union member is added without a handler, TypeScript raises a type error. ```ts function assertNever(x: never): never { throw new Error(`Unexpected value: ${x}`); } function describeShape(s: Shape): string { switch (s.kind) { case "circle": return "A circle"; case "rectangle": return "A rectangle"; default: return assertNever(s); // Error if Shape has unhandled members } } ``` This pattern ensures that adding a new variant to the `Shape` type forces developers to update every switch that uses `assertNever`, making the codebase more resilient to change.
`satisfies` validates that an expression matches a type without widening the inferred type to that type. This gives you both type-checking and the benefit of keeping the more precise inferred type. ```ts type Palette = Record<string, string | [number, number, number]>; const palette = { red: [255, 0, 0], green: "#00ff00", } satisfies Palette; // palette.red is inferred as [number, number, number], not string | [number, number, number] palette.red[0]; // OK palette.green.toUpperCase(); // OK — still string, not string | [...] ``` Before `satisfies`, you had to choose between type annotation (loses precision) or casting (loses safety). `satisfies` gives you both: the check happens at the declaration site, but the inferred type retains its specificity.
`const` type parameters (using the `const` modifier before a generic type parameter) infer the argument as `const`, preserving literal types and tuple structure rather than widening to primitives. ```ts function toTuple<const T extends readonly unknown[]>(arr: T): T { return arr; } const result = toTuple([1, "two", true]); // Without const: (number | string | boolean)[] // With const: readonly [1, "two", true] ``` Previously you needed `as const` at every call site to achieve this narrowing. `const` type parameters push that inference into the function definition, making APIs cleaner for consumers who want literal/tuple types from array literals.
Variadic tuple types (TS 4.0) allow spread elements in tuple type positions, enabling generic manipulation of tuple prefixes, suffixes, and concatenation. ```ts type Concat<A extends unknown[], B extends unknown[]> = [...A, ...B]; type C = Concat<[1, 2], [3, 4]>; // [1, 2, 3, 4] function prepend<T, U extends unknown[]>(item: T, rest: U): [T, ...U] { return [item, ...rest]; } type Unshift<T extends unknown[], U> = [U, ...T]; ``` Variadic tuples unlocked proper typing for higher-order functions that compose argument lists, such as `bind`, `curry`, and middleware pipelines.
Labeled tuple elements (TS 4.0) attach names to positional tuple members for improved readability in IDE tooltips and error messages. Labels have no effect on structural compatibility. ```ts type Range = [start: number, end: number]; type Callback = [handler: () => void, delay?: number]; function makeRange(start: number, end: number): Range { return [start, end]; } ``` When a tuple is used as a rest parameter, labels appear in function signature hints in editors: ```ts function setTimer(...[handler, delay]: Callback): void { /* ... */ } ``` All elements must be labeled or unlabeled — you cannot mix.
Rest elements in tuples (using `...T[]`) let you express tuples with a variable-length section. Since TS 4.2, rest elements can appear anywhere in a tuple, not just at the end. ```ts type AtLeastOne<T> = [T, ...T[]]; type WithMiddle = [string, ...number[], boolean]; const a: AtLeastOne<number> = [1]; // OK const b: AtLeastOne<number> = [1, 2, 3]; // OK // const c: AtLeastOne<number> = []; // Error ``` This enables precise typing for patterns like "at least one argument", or functions that accept a known prefix and suffix with variadic middle, which was previously impossible to express accurately.
`as const` recursively marks every property as `readonly` and narrows all literals to their exact types instead of widening to primitives. Arrays become `readonly` tuples, string values become string literal types, and numbers become number literal types. ```ts const config = { env: "production", ports: [3000, 3001], } as const; // type: { readonly env: "production"; readonly ports: readonly [3000, 3001] } type Env = typeof config.env; // "production" — not string ``` This is particularly useful for creating lookup maps or default option bags that downstream code should treat as immutable constants. Attempting to mutate any property causes a compile-time error.
An assertion function has a return type of `asserts x is T` (or `asserts condition`). If the function returns normally (does not throw), TypeScript narrows the type of `x` to `T` in the calling scope. This is similar to a type guard, but the narrowing is unconditional and persists beyond the call. ```ts function assertIsString(val: unknown): asserts val is string { if (typeof val !== "string") throw new TypeError(`Expected string, got ${typeof val}`); } function process(input: unknown) { assertIsString(input); input.toUpperCase(); // narrowed to string after the assertion } ``` Assertion functions work well with validation schemas and defensive checks in constructors, where you want narrowing to carry over to all subsequent code rather than just inside an `if` block.
A user-defined type guard is a function that returns a boolean and has a special return type `x is T`. When TypeScript sees `if (isT(x))`, it narrows `x` to `T` inside the truthy branch. ```ts function isError(value: unknown): value is Error { return value instanceof Error; } function handle(result: string | Error) { if (isError(result)) { console.error(result.message); // narrowed to Error } else { console.log(result.toUpperCase()); // narrowed to string } } ``` TypeScript trusts user-defined type guards completely — they shift responsibility to the developer. There is no compile-time verification that the runtime logic is correct, so incorrect type guards can cause unsound behavior.
Module augmentation allows you to add new declarations to an existing module without modifying its source, using a `declare module "module-name" {}` block inside a normal `.ts` or `.d.ts` file. ```ts import "express"; declare module "express" { interface Request { user?: { id: string; role: string }; } } // Now req.user is typed everywhere Express Request is used ``` You must have an `import` or `export` in the file for TypeScript to treat it as a module (rather than a script with global declarations). Module augmentation is essential for adding types to middleware-injected properties, global stores, and polyfills.
When the same interface is declared multiple times, TypeScript merges all method signatures. Overloaded signatures from later declarations are placed earlier in the merged list (except for signatures with string literal parameters, which are kept first). ```ts interface Formatter { format(x: number): string; } interface Formatter { format(x: Date): string; format(x: string): string; } // Merged result: // format(x: string): string — literal last-declared first // format(x: Date): string // format(x: number): string ``` This ordering affects overload resolution — TypeScript tries signatures in order. Understanding merging order prevents subtle bugs when augmenting library interfaces.
TypeScript namespaces can be merged with other namespaces, classes, functions, and enums of the same name. This allows adding static properties or type aliases to classes or appending values to enums. ```ts function createPoint(x: number, y: number): createPoint.Point { return { x, y }; } namespace createPoint { export interface Point { x: number; y: number } export const origin: Point = { x: 0, y: 0 }; } const p = createPoint.origin; // { x: 0, y: 0 } ``` This pattern is common in declaration files for JavaScript libraries where a function also acts as a namespace. It is less common in modern TypeScript where ES modules and classes cover most use cases.
Ambient declarations describe types and values that exist at runtime but are defined outside TypeScript (e.g., globals injected by a bundler, browser APIs not in `lib`, or global scripts). They use the `declare` keyword and do not emit any JavaScript. ```ts declare const __VERSION__: string; // e.g., injected by webpack declare function require(id: string): any; // CommonJS require declare module "*.svg" { const content: string; export default content; } ``` Ambient declarations live in `.d.ts` files or in `.ts` files without any imports/exports (global script files). They are the bridge between TypeScript's type world and raw JavaScript environments.
TypeScript allows a special fake `this` parameter as the first parameter of a function to constrain the type of `this` inside that function. It is erased during compilation and not counted as a real parameter. ```ts interface User { name: string; greet(this: User): string; } const user: User = { name: "Alice", greet() { return `Hello, ${this.name}`; }, }; const fn = user.greet; // fn(); // Error: `this` context is not User fn.call(user); // OK ``` This prevents accidental detached method calls where `this` would be `undefined` (in strict mode) or the global object. The `noImplicitThis` flag raises an error when `this` has an implicit `any` type.
Method signatures (`method(): void`) and property function types (`method: () => void`) look similar but differ in strictness. With `strictFunctionTypes`, property function types are checked contravariantly in parameter types, while method signatures are checked bivariantly (less strict) for backwards compatibility. ```ts interface A { methodSig(x: string): void; // bivariant — less strict propFn: (x: string) => void; // contravariant — stricter } ``` In practice, prefer property function types (`propFn: (x: T) => void`) in interface definitions for maximum type safety. Method syntax is allowed to be bivariant because arrays like `Array<T>` use method syntax and enforcing contravariance would break many existing programs.
A type position is covariant if a subtype can substitute a supertype (return types, read-only positions). It is contravariant if a supertype must substitute a subtype (parameter positions, write-only positions). TypeScript enforces this with `strictFunctionTypes`. ```ts type Animal = { name: string }; type Dog = Animal & { breed: string }; // Covariant (return types) — Dog → Animal OK type GetAnimal = () => Animal; const getDog: GetAnimal = (): Dog => ({ name: "Rex", breed: "Lab" }); // OK // Contravariant (parameters) — Animal → Dog callback required type HandleDog = (d: Dog) => void; const handleAnimal: HandleDog = (a: Animal) => {}; // OK (accepts less specific) ``` Intuition: a function that accepts any `Animal` is safe to use where a `Dog` handler is expected, because every `Dog` is an `Animal`. But a function that returns an `Animal` is unsafe where a `Dog` is expected.
`strict: true` enables eight compiler flags simultaneously: 1. `strictNullChecks` — `null`/`undefined` are not assignable to other types. 2. `strictFunctionTypes` — function parameter types are checked contravariantly. 3. `strictBindCallApply` — `bind`, `call`, `apply` are strictly typed. 4. `strictPropertyInitialization` — class properties must be initialized in the constructor. 5. `noImplicitAny` — implicit `any` inferences are errors. 6. `noImplicitThis` — `this` with implicit `any` type is an error. 7. `alwaysStrict` — emits `"use strict"` in all output files. 8. `useUnknownInCatchVariables` — caught errors are typed as `unknown` instead of `any` (TS 4.4). Enabling all of these together provides the strongest type-safety guarantees TypeScript offers. New projects should always start with `strict: true`.
`baseUrl` sets the root directory for resolving non-relative module specifiers. `paths` maps module path patterns to file system locations, enabling import aliases. ```json { "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["src/*"], "@components/*": ["src/components/*"] } } } ``` With this config, `import Button from "@components/Button"` resolves to `src/components/Button`. Important gotcha: `paths` is a TypeScript-only resolution hint — your bundler (Webpack, Vite, esbuild) needs corresponding alias configuration separately, because TypeScript does not rewrite import paths in emitted JavaScript.
Project references (`references` in tsconfig) let you split a TypeScript codebase into multiple sub-projects that can be built incrementally and in dependency order using `tsc --build` (or `tsc -b`). Each referenced project must have `composite: true` enabled. ```json // tsconfig.json (root) { "references": [ { "path": "./packages/core" }, { "path": "./packages/ui" } ] } ``` Project references produce separate `.d.ts` output for each sub-project, so dependents consume type declarations rather than re-compiling source. This dramatically speeds up `tsc --build` in large monorepos and enables parallel builds.
Decorators are a stage-3 TC39 proposal enabled via `experimentalDecorators: true`. They are functions applied with `@decorator` syntax that receive metadata about the decorated target. ```ts function log(target: any, key: string, desc: PropertyDescriptor) { const orig = desc.value; desc.value = function (...args: any[]) { console.log(`Calling ${key}`); return orig.apply(this, args); }; } class Service { @log fetch(url: string) { /* ... */ } } ``` - **Class decorator**: receives the constructor. - **Method decorator**: receives `target`, `propertyKey`, `PropertyDescriptor`. - **Property decorator**: receives `target` and `propertyKey`, no descriptor. - **Parameter decorator**: receives `target`, `methodKey`, and parameter index. TS 5.0 introduced the new stable decorator spec (without `experimentalDecorators`) which is not backwards compatible.
An `abstract` class cannot be instantiated directly — it serves as a base class that subclasses must extend. `abstract` methods declare a signature without an implementation body; concrete subclasses must provide an implementation. ```ts abstract class Shape { abstract area(): number; describe(): string { return `Area: ${this.area()}`; } } class Circle extends Shape { constructor(private r: number) { super(); } area() { return Math.PI * this.r ** 2; } } // new Shape(); // Error: cannot instantiate abstract class new Circle(5); // OK ``` Abstract classes sit between interfaces (pure contract, no implementation) and concrete classes (full implementation). They are useful when you want to share some logic but enforce that subclasses provide certain methods.
TypeScript does not support multiple inheritance, but you can simulate it with mixins — functions that take a class and return a new class with additional methods merged in. ```ts type Constructor<T = {}> = new (...args: any[]) => T; function Serializable<TBase extends Constructor>(Base: TBase) { return class extends Base { serialize() { return JSON.stringify(this); } }; } function Validatable<TBase extends Constructor>(Base: TBase) { return class extends Base { validate() { return true; } }; } class User { constructor(public name: string) {} } const RichUser = Serializable(Validatable(User)); const u = new RichUser("Alice"); u.serialize(); u.validate(); ``` The key constraint is `TBase extends Constructor`, which ensures the mixin can be applied to any class. TypeScript correctly infers the composed type.
TypeScript does not support `T extends A & B` directly as separate extends clauses, but you can use an intersection type in a single constraint. ```ts interface Serializable { serialize(): string } interface Loggable { log(): void } function process<T extends Serializable & Loggable>(item: T): string { item.log(); return item.serialize(); } ``` For multiple independent constraints across different type parameters use separate `extends` clauses: ```ts function merge<T extends object, U extends object>(a: T, b: U): T & U { return { ...a, ...b }; } ``` Intersecting constraints in a single `extends` clause is the cleanest approach when all constraints apply to the same type parameter.
Generic type parameters can have defaults using `= DefaultType`, making them optional at the use site. The default is used when the parameter is not explicitly provided and cannot be inferred. ```ts interface ApiResponse<T = unknown, E = Error> { data: T; error: E | null; } const res: ApiResponse = { data: "ok", error: null }; // T=unknown, E=Error const typed: ApiResponse<User> = { data: user, error: null }; // E=Error ``` Defaults reduce verbosity in generic interfaces and component props. They are required to follow the same constraints as the parameter (`T extends X = DefaultExtendingX`).
TypeScript does not natively support higher-kinded types (type constructors parameterised by other type constructors), but you can simulate them via an interface that acts as a "type constructor registry" and mapped lookup. ```ts interface HKT { "Array": Array<this["_A"]>; "Promise": Promise<this["_A"]>; "_A": unknown; // placeholder } type Apply<F extends keyof HKT, A> = (HKT & { _A: A })[F]; type ArrayOfString = Apply<"Array", string>; // string[] type PromiseOfBool = Apply<"Promise", boolean>; // Promise<boolean> ``` This "defunctionalisation" trick, popularised by libraries like `fp-ts`, lets you write generic functions that abstract over a type constructor (e.g., "map over any functor") at the cost of some boilerplate.
`infer R` introduces a free type variable `R` that TypeScript unifies with the matched portion of the type when the `extends` condition succeeds. You can place `infer` anywhere in the pattern. ```ts type Unpacked<T> = T extends (infer U)[] ? U : T extends (...args: any[]) => infer R ? R : T extends Promise<infer P> ? P : T; type A = Unpacked<string[]>; // string type B = Unpacked<() => number>; // number type C = Unpacked<Promise<boolean>>; // boolean type D = Unpacked<string>; // string ``` Multiple `infer` variables in a single conditional type capture different parts simultaneously. When the same `infer` variable appears multiple times in a covariant position, TypeScript unions the captured types.
TypeScript's structural type system allows two types with identical shapes to be used interchangeably, which can cause bugs when you mix logically distinct types (e.g., user IDs and product IDs). Branded types add a phantom property to make types nominally distinct. ```ts type Brand<T, B> = T & { readonly __brand: B }; type UserId = Brand<string, "UserId">; type ProductId = Brand<string, "ProductId">; function getUser(id: UserId) { /* ... */ } const uid = "u123" as UserId; const pid = "p456" as ProductId; getUser(uid); // OK // getUser(pid); // Error: ProductId not assignable to UserId ``` The `__brand` property exists only at the type level and is never set at runtime — the `as` cast is used to create branded values, typically wrapped in a factory function that performs validation.
`Awaited<T>` recursively unwraps `Promise`-like types, resolving nested promises to their final value type. It was introduced in TS 4.5 to replace `ReturnType` patterns that only unwrapped one level. ```ts type A = Awaited<Promise<string>>; // string type B = Awaited<Promise<Promise<number>>>; // number type C = Awaited<boolean | Promise<string>>; // boolean | string async function fetchData(): Promise<User[]> { /* ... */ } type Data = Awaited<ReturnType<typeof fetchData>>; // User[] ``` `Awaited` also handles non-native "thenable" objects (anything with a `.then` method), not just native Promises. It is used internally by TypeScript to type `await` expressions and `Promise.all`/`Promise.race` return types.
`NoInfer<T>` prevents TypeScript from using a type position as a source of inference for the generic type parameter. This lets you constrain an argument to already-inferred `T` without widening `T` to accommodate that argument. ```ts // Without NoInfer, default widens T to include "invalid" function createStore<T>(initial: T, fallback: NoInfer<T>): T { return initial ?? fallback; } const store = createStore("active", "inactive"); // T = string // createStore("active", 99); // Error: number not assignable to NoInfer<string> ``` Before `NoInfer`, the workaround was `T & {}` or an extra generic constraint. `NoInfer` makes the intent explicit and is especially useful in APIs where one argument defines the type and others should conform to it without influencing inference.
`DeepPartial<T>` recursively makes every property optional at every level. The tricky parts are handling arrays (whose elements should also be deep-partial) and avoiding infinite recursion on primitive types. ```ts type DeepPartial<T> = T extends (infer U)[] ? DeepPartial<U>[] : T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>> : T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T; type Config = { db: { host: string; port: number }; retries: number }; type PartialConfig = DeepPartial<Config>; // { db?: { host?: string; port?: number }; retries?: number } ``` The primitive branch (`T` itself) is essential — without it, strings, numbers, and booleans would match the `object` branch (they don't, but `typeof null === "object"` means null handling matters too). Checking for arrays before objects prevents array index keys from being made optional. For production use, also handle `Map`, `Set`, `Date`, and `Function` to avoid incorrectly recursing into them.
`DeepReadonly<T>` recursively makes every property `readonly` at every nesting level. Handling arrays and primitive escape hatches is the same discipline as `DeepPartial`. ```ts type DeepReadonly<T> = T extends (infer U)[] ? ReadonlyArray<DeepReadonly<U>> : T extends Map<infer K, infer V> ? ReadonlyMap<K, DeepReadonly<V>> : T extends Set<infer U> ? ReadonlySet<DeepReadonly<U>> : T extends object ? { readonly [K in keyof T]: DeepReadonly<T[K]> } : T; type State = DeepReadonly<{ user: { name: string; scores: number[] } }>; const s: State = { user: { name: "Alice", scores: [1, 2] } }; // s.user.name = "Bob"; // Error — deeply readonly ``` The `Map` and `Set` branches are critical in real applications. Without them, `map.set(k, v)` on a `DeepReadonly<Map<K, V>>` would not be caught because `ReadonlyMap` removes `set` and `delete`. Functions should be excluded from recursion to avoid breaking callable types.
We split the path string recursively using template literal types and conditional types, then use indexed access to traverse the object type. ```ts type Get<T, P extends string> = P extends `${infer K}.${infer Rest}` ? K extends keyof T ? Get<T[K], Rest> : never : P extends keyof T ? T[P] : never; type Data = { user: { profile: { name: string; age: number } } }; type Name = Get<Data, "user.profile.name">; // string type Age = Get<Data, "user.profile.age">; // number type Bad = Get<Data, "user.missing">; // never ``` This is a fully type-level recursive descent parser over dotted path strings. The key insight is using `infer K` to split on the first dot and `infer Rest` for the remainder. TypeScript evaluates this lazily and terminates when the path is a single segment (no dot). Libraries like Zod, react-hook-form, and ts-essentials use variants of this pattern for deep field access.
This relies on the contravariant position of function parameters. When the same type variable appears in multiple contravariant positions, TypeScript infers its intersection. ```ts type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never; type A = UnionToIntersection<{ a: 1 } | { b: 2 }>; // { a: 1 } & { b: 2 } type B = UnionToIntersection<string | number>; // string & number = never ``` The first step distributes `U` into a union of function types `(k: T) => void`. The second step uses `infer I` in contravariant position on that union, which forces TypeScript to compute the intersection of all `T` types to satisfy all function signatures simultaneously. This is a foundational trick used in many advanced type utilities.
Extracting the last union member requires converting the union to an intersection of function overloads, then using `infer` on the last overload signature. This exploits how TypeScript resolves overloads (last signature wins). ```ts type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never; type LastOfUnion<T> = UnionToIntersection<T extends any ? () => T : never> extends () => infer R ? R : never; type L = LastOfUnion<"a" | "b" | "c">; // "c" (order is internal) ``` Important caveat: "last" here is the last in TypeScript's internal union ordering, which is not guaranteed to be stable across compiler versions or declaration order. This type is used as a building block for `UnionToTuple` but should not be used to reliably extract a specific member by position in production code.
Combining `LastOfUnion` and `Exclude`, you can iteratively pop the last union member into a tuple via a recursive conditional type. ```ts type UnionToTuple<T, L = LastOfUnion<T>> = [T] extends [never] ? [] : [...UnionToTuple<Exclude<T, L>>, L]; type T = UnionToTuple<"a" | "b" | "c">; // ["a", "b", "c"] — order depends on internal union ordering ``` This pattern is educational and used in type-level programming puzzles. Production use is discouraged because: (1) union ordering in TypeScript is implementation-defined, (2) deep recursion on large unions hits TypeScript's instantiation depth limit, and (3) any change in compiler version may silently change tuple order. Use explicit tuple types instead when you need ordered lists.
`never` cannot be detected with a simple `T extends never ? true : false` because conditional types distribute over `never`, making the entire type resolve to `never` instead of `true` or `false`. The fix is to wrap both sides in a tuple to prevent distribution. ```ts type IsNever<T> = [T] extends [never] ? true : false; type A = IsNever<never>; // true type B = IsNever<string>; // false type C = IsNever<string | never>; // false (never is identity for |) // Wrong approach: type Bad<T> = T extends never ? true : false; type D = Bad<never>; // never — not true! ``` The `[T] extends [never]` trick wraps `T` in a single-element tuple, preventing TypeScript from distributing the conditional over `never`. This is a fundamental building block for many type-level utilities that need to detect impossible types.
`any` is special: it is assignable to and from every type, including `never`. We exploit this by checking whether `0 extends (1 & T)` — when `T` is `any`, `1 & any` is `any`, and `0 extends any` is `true`. For all other types, `1 & T` is either `1` (if `T` is `any`) or a narrower type, and `0 extends 1` is `false`. ```ts type IsAny<T> = 0 extends (1 & T) ? true : false; type A = IsAny<any>; // true type B = IsAny<unknown>; // false type C = IsAny<never>; // false type D = IsAny<string>; // false ``` Alternatively, `[any] extends [T] ? [T] extends [any] ? true : false : false` works but is wordier. `IsAny` is used in type-level test suites (like `expect-type`) to catch cases where a generic accidentally widens to `any`, which would silence all downstream errors.
`Prettify<T>` (also called `Expand<T>`) forces TypeScript to eagerly evaluate and flatten an intersection or complex type into a single object type, making IDE tooltips and error messages more readable. ```ts type Prettify<T> = { [K in keyof T]: T[K] } & {}; type A = { id: number } & { name: string } & { email: string }; type B = Prettify<A>; // { id: number; name: string; email: string } ``` Without `Prettify`, hovering over a type built from multiple intersections shows the raw intersection chain, which is hard to read. `Prettify` causes TypeScript to perform the intersection and display the merged object. The `& {}` at the end is critical — without it, TypeScript's homomorphic mapped type optimization would not trigger the flattening. This is a developer experience utility with no runtime cost.
Define an event map type and use it to constrain the `on`, `off`, and `emit` methods so that listeners receive exactly the correct payload type for each event. ```ts type EventMap = Record<string, unknown>; class TypedEmitter<Events extends EventMap> { private handlers: { [K in keyof Events]?: Array<(payload: Events[K]) => void> } = {}; on<K extends keyof Events>(event: K, handler: (p: Events[K]) => void): this { (this.handlers[event] ??= []).push(handler); return this; } emit<K extends keyof Events>(event: K, payload: Events[K]): void { this.handlers[event]?.forEach(h => h(payload)); } } type AppEvents = { login: { userId: string }; logout: void }; const emitter = new TypedEmitter<AppEvents>(); emitter.on("login", ({ userId }) => console.log(userId)); emitter.emit("login", { userId: "u1" }); // emitter.emit("login", { wrong: true }); // Error ``` The `[K in keyof Events]?` mapped type ensures the handler map is structurally aligned to `Events`. Returning `this` from `on` enables fluent chaining. This pattern is used in libraries like `typed-emitter` and `eventemitter3`.
Use a generic accumulator type parameter that grows with each builder method call, so the final `.build()` call has full type information about what was configured. ```ts class QueryBuilder<T extends object = {}> { private clauses: Partial<T> = {}; select<K extends string, V>(key: K, value: V): QueryBuilder<T & Record<K, V>> { (this.clauses as any)[key] = value; return this as any; } build(): T { return this.clauses as T; } } const query = new QueryBuilder() .select("name", "Alice") .select("age", 30) .build(); // type: { name: string; age: number } console.log(query.name, query.age); ``` The key insight is that `select` returns `QueryBuilder<T & Record<K, V>>`, expanding `T` with each call. The `as any` casts in the implementation are necessary because TypeScript cannot verify the accumulator pattern internally, but the public API remains fully type-safe. This pattern is used by ORMs like Drizzle and Prisma query builders.
In fluent APIs, each method returns a new type that reflects the operation performed, enabling the type system to track cumulative state across method chains. The trick is that the return type of each method is a more specific variant of `this`. ```ts type Validated<T> = T & { __validated: true }; type Sanitized<T> = T & { __sanitized: true }; class Pipeline<T> { constructor(private value: T) {} validate(fn: (v: T) => boolean): Pipeline<Validated<T>> { if (!fn(this.value)) throw new Error("Invalid"); return this as any; } sanitize(fn: (v: T) => T): Pipeline<Sanitized<T>> { return new Pipeline(fn(this.value)) as any; } getValue(this: Pipeline<Validated<T> & Sanitized<T>>): T { return this.value; } } ``` The `getValue` method is constrained to only be callable when both `__validated` and `__sanitized` brands are present, enforced at compile time. This pattern guarantees method call order at the type level without any runtime overhead.
TypeScript's type inference is a constraint-based system. When you call a generic function, the compiler collects inference candidates for each type parameter from the argument types, then solves the constraint set. Inference candidates come from covariant positions (e.g., argument types, return type assignments) and are collected into a list. TypeScript then unifies them: for covariant candidates it takes the union; for contravariant candidates (function parameter positions with `strictFunctionTypes`) it takes the intersection. ```ts function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> { // T inferred from obj, K inferred from keys return {} as Pick<T, K>; } pick({ a: 1, b: "x" }, ["a"]); // T = {a:number,b:string}, K = "a" ``` Fixpoint iteration: TypeScript repeats the inference pass up to a fixed limit when circular constraints arise (e.g., recursive generic functions). `infer` variables are resolved by matching structural patterns. Deferred inference (e.g., inside conditional types on unresolved type parameters) is resolved lazily when the parameter is finally constrained. Understanding this algorithm explains why some type inference "fails" and requires explicit annotations.
A `.d.ts` file mirrors the public API of a JS module using `declare` statements. For a CommonJS module use `export =`; for ES modules use named/default exports. ```ts // math-utils.d.ts declare module "math-utils" { export function clamp(value: number, min: number, max: number): number; export function lerp(a: number, b: number, t: number): number; export const PI: number; export default clamp; } ``` For global scripts: ```ts // global.d.ts declare function trackEvent(name: string, props?: Record<string, unknown>): void; declare const APP_ENV: "dev" | "staging" | "prod"; ``` Best practices: (1) Use `export =` only for CJS `module.exports = ...` patterns. (2) Avoid `any` — use `unknown` or specific types. (3) Add JSDoc comments — they surface in editor tooltips. (4) Submit to `@types/*` via DefinitelyTyped for popular libraries. (5) Set `types` or `typesVersions` in `package.json` to point to the `.d.ts` entry.
`strictFunctionTypes` (part of `strict`) checks function type parameters contravariantly rather than bivariantly. This means a callback parameter type must be a supertype (or equal) of the expected parameter — you cannot pass a handler that requires a more specific type than the caller provides. ```ts type Handler = (e: Event) => void; type MouseHandler = (e: MouseEvent) => void; let h: Handler; let m: MouseHandler; // MouseEvent extends Event m = h; // OK: Handler accepts any Event, MouseHandler needs MouseEvent // h = m; // Error (strict): m expects MouseEvent, but Handler may call with plain Event ``` Bivariant checking (method syntax) was the default because Array methods like `push` and `forEach` use method syntax and would break under strict variance. The practical consequence: always use arrow function property syntax (`prop: (x: T) => void`) in interfaces where you want strict callback checking.
A `readonly` tuple parameter means the function cannot mutate the array in-place. More importantly, it widens the accepted argument type — a mutable tuple is assignable to a readonly tuple, but not vice versa. ```ts function sum(nums: readonly number[]): number { return nums.reduce((a, b) => a + b, 0); } const mutable: number[] = [1, 2, 3]; const frozen = [1, 2, 3] as const; sum(mutable); // OK — mutable is assignable to readonly sum(frozen); // OK function pop(nums: number[]): number | undefined { return nums.pop(); } // pop(frozen); // Error: readonly not assignable to mutable ``` Preferring `readonly` parameters documents intent (function does not mutate), enables passing `as const` literals, and works correctly with `strictFunctionTypes` variance rules. This is especially important for React hooks and pure functions in state management.
`as T` is a type assertion — it forcibly tells the compiler "treat this value as type T" with no compile-time check that the assertion is valid (only a check that `T` and the actual type overlap). `satisfies T` validates that the expression matches `T` but does not change the inferred type. ```ts type Color = string | [number, number, number]; // `as` — loses precision, silences errors: const c1 = { red: [255, 0, 0], blue: "blue" } as Record<string, Color>; c1.red[0]; // TypeScript thinks it might be a string — unsafe // `satisfies` — keeps precision, validates: const c2 = { red: [255, 0, 0], blue: "blue" } satisfies Record<string, Color>; c2.red[0]; // correctly typed as number c2.blue.toUpperCase(); // correctly typed as string ``` Rule of thumb: use `satisfies` for declaration-site validation with preserved specificity. Use `as` only when you have information the compiler cannot infer and you accept the responsibility for correctness.
TypeScript's type system is Turing-complete. You can encode natural numbers as tuple lengths and implement arithmetic at the type level — an analogue of Church numerals from lambda calculus. ```ts type BuildTuple<N extends number, T extends unknown[] = []> = T["length"] extends N ? T : BuildTuple<N, [...T, 0]>; type Add<A extends number, B extends number> = [...BuildTuple<A>, ...BuildTuple<B>]["length"]; type Inc<N extends number> = Add<N, 1>; type Mul<A extends number, B extends number> = B extends 0 ? 0 : Add<A, Mul<A, Exclude<B, 0> extends number ? Exclude<B, 0> : never>>; type Two = Add<1, 1>; // 2 type Six = Mul<2, 3>; // 6 ``` This works by building tuples of length N and concatenating/slicing them. TypeScript limits tuple lengths (~1000) and recursion depth (~100 hops), so this is academic rather than practical. Understanding it deepens intuition about how conditional and recursive types interact with the compiler's constraint solver.
We parse the route pattern at the type level using template literal types and `infer` to extract all `:param` segments into a params object type. ```ts type ExtractParams<Path extends string> = Path extends `${infer _Start}:${infer Param}/${infer Rest}` ? { [K in Param | keyof ExtractParams<`/${Rest}`>]: string } : Path extends `${infer _Start}:${infer Param}` ? { [K in Param]: string } : {}; type Params = ExtractParams<"/user/:id/post/:postId">; // { id: string; postId: string } function route<P extends string>(path: P, params: ExtractParams<P>): string { return path.replace(/:([\w]+)/g, (_, k) => (params as any)[k]); } route("/user/:id/post/:postId", { id: "1", postId: "2" }); // OK // route("/user/:id", {}); // Error: id missing ``` This is the core pattern used by type-safe routers like `react-router v6`, `tanstack-router`, and `trpc`. Extending it to handle optional params, query strings, and nested routes follows the same recursive template literal approach.
The parsing uses recursive conditional types with `infer` to split the path on `:` and `/` boundaries, accumulating extracted parameter names into a union, then mapping to an object type. ```ts type ParseParams<Path extends string> = Path extends `:${infer Param}/${infer Rest}` ? { [K in Param]: string } & ParseParams<Rest> : Path extends `:${infer Param}` ? { [K in Param]: string } : Path extends `${infer _}/${infer Rest}` ? ParseParams<Rest> : {}; type Prettify<T> = { [K in keyof T]: T[K] } & {}; type Params = Prettify<ParseParams<"user/:id/:name">>; // { id: string; name: string } ``` Each recursive branch either (1) extracts a param and recurses on the remainder, (2) extracts the final param, or (3) skips a non-param segment. The intersection of all extracted `{ K: string }` objects builds the final params object. `Prettify` is used to flatten the intersection for display.
`infer` can extract the type returned by a class constructor or a static method, enabling dynamic typing of factory patterns. ```ts type ConstructorReturn<T extends abstract new (...args: any) => any> = T extends abstract new (...args: any) => infer R ? R : never; class UserService { static create() { return new UserService(); } getUser(id: string): { id: string; name: string } { return null!; } } type ServiceInstance = ConstructorReturn<typeof UserService>; // UserService type GetUserReturn = ReturnType<UserService["getUser"]>; // { id: string; name: string } ``` For abstract classes you need `abstract new` in the constraint. This is used in dependency injection containers, plugin systems, and test factories that instantiate classes generically. `InstanceType<typeof Cls>` is the built-in equivalent of `ConstructorReturn` for non-abstract classes.
The TypeScript Compiler API exposes the full compilation pipeline programmatically. A custom transformer is a factory function that receives a `TransformationContext` and returns a node visitor. ```ts import * as ts from "typescript"; function removeConsoleTransformer(ctx: ts.TransformationContext) { return (sourceFile: ts.SourceFile): ts.SourceFile => { function visit(node: ts.Node): ts.Node { if ( ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.expression.getText() === "console" ) { return ts.factory.createVoidZero(); } return ts.visitEachChild(node, visit, ctx); } return ts.visitNode(sourceFile, visit) as ts.SourceFile; }; } ``` Transformers plug in via `ts.transpileModule({ transformers: { before: [removeConsoleTransformer] } })` or through `ts-patch`/`ttypescript` for project-wide use. The AST node kinds are exposed on `ts.SyntaxKind`. Custom transformers power tools like `ts-jest`, `typia` (runtime validators from types), and i18n string extraction.
Zod schemas are generic TypeScript classes/objects where the inferred type is encoded in a type parameter. The `z.infer<typeof schema>` utility extracts it. ```ts import { z } from "zod"; const UserSchema = z.object({ id: z.string().uuid(), age: z.number().min(0), role: z.enum(["admin", "user"]), }); type User = z.infer<typeof UserSchema>; // { id: string; age: number; role: "admin" | "user" } const parsed = UserSchema.parse(JSON.parse(rawInput)); // parsed is typed as User with runtime validation ``` Zod achieves this by making every schema method (`string()`, `object()`, `array()`) return a generic class `ZodType<Output, Def, Input>` where `Output` carries the TypeScript type. `z.infer<T>` is simply `T["_output"]`. The runtime validation and the TypeScript type are derived from the same source of truth, eliminating type/validation drift. `io-ts` uses a similar HKT-based approach; `Valibot` uses a more explicit type parameter threading.
`ts-morph` wraps the TypeScript Compiler API with a higher-level, mutable AST manipulation API, making it practical to write codegen scripts without deep compiler knowledge. ```ts import { Project } from "ts-morph"; const project = new Project(); const src = project.addSourceFileAtPath("src/models/user.ts"); for (const iface of src.getInterfaces()) { const genFile = project.createSourceFile( `src/generated/${iface.getName()}.schema.ts`, { overwrite: true } ); genFile.addImportDeclaration({ moduleSpecifier: "zod", defaultImport: "{ z }" }); // Build zod schema from interface properties... } project.saveSync(); ``` `ts-morph` exposes intuitive methods like `getProperties()`, `getType()`, `addFunction()`, and `insertText()`. Common use cases: generating Zod schemas from interfaces, deriving API client types from Express route handlers, creating GraphQL type definitions from TypeScript models, and bulk refactoring (renaming, adding decorators). It maintains source maps and preserves formatting when using `save()` with Prettier.
Haskell's type classes define abstract interfaces (like `Functor`, `Monad`) that types opt into via explicit instance declarations, with dictionary-passing at runtime. TypeScript uses structural typing with no opt-in — any type satisfying a structural shape is implicitly an "instance". TypeScript simulates type classes with interfaces + generics: ```ts interface Functor<F extends { map: (...a: any[]) => any }> { map<A, B>(fa: F, f: (a: A) => B): F; } ``` Key differences: 1. TypeScript has no concept of type class coherence — two modules can provide conflicting "instances" with no conflict detection. 2. Haskell type classes are resolved at compile time via dictionary passing; TypeScript relies on object interfaces. 3. Higher-kinded types are native in Haskell; TypeScript requires the defunctionalisation workaround. 4. TypeScript's structural typing means implicit satisfaction; Haskell requires explicit `deriving` or `instance` declarations. Libraries like `fp-ts` bridge the gap by encoding type classes as explicit dictionary objects (`Functor<F>` instances passed as arguments), achieving many of the same abstractions at the cost of verbosity.
`--isolatedModules` requires every file to be independently transpilable without needing to read other files. Tools like Babel, esbuild, and SWC transpile one file at a time without a full type-checking pass, making this constraint a compatibility requirement. `const enum` values are inlined at usage sites by the TypeScript compiler: it reads the enum declaration from another file and replaces all usages with the literal numeric values. A single-file transpiler cannot do this because it does not read the declaration file. ```ts // In isolatedModules mode: const enum Dir { Up = 0, Down = 1 } // Error: cannot be inlined // Fix 1: use regular enum enum Dir { Up = 0, Down = 1 } // Fix 2: use string literal union type Dir = "Up" | "Down"; ``` Solution: either use regular `enum`, string union types, or enable `preserveConstEnums: true` which emits the enum object at runtime. The `verbatimModuleSyntax` flag (TS 5.0) also enforces import hygiene that helps with isolatedModules.
`verbatimModuleSyntax` is a tsconfig flag (TS 5.0) that enforces a simpler, predictable module syntax rule: every `import`/`export` is emitted verbatim to the output (no transformation), so type-only imports must always use `import type` and cannot be automatically elided. ```ts // verbatimModuleSyntax: true import { User } from "./types"; // Error if User is only used as a type import type { User } from "./types"; // OK — explicitly type-only import { createUser, type User } from "./users"; // inline type modifier OK ``` This replaces the older `importsNotUsedAsValues` and `preserveValueImports` flags with a single cleaner rule. It prevents the situation where TypeScript silently elides an import that a bundler expected to be a side-effect import. It is especially important when using esbuild or Babel (isolated-modules-style transpilers) that need explicit `import type` to know what to strip. Next.js, Vite, and most modern toolchains benefit from this setting.
`using` (and `await using` for async) implements the TC39 Explicit Resource Management proposal. A value declared with `using` must implement the `Symbol.dispose` method; TypeScript calls it automatically when the block exits, even if an exception is thrown. ```ts class FileHandle { constructor(public path: string) { console.log(`Open: ${path}`); } [Symbol.dispose]() { console.log(`Close: ${this.path}`); } } function processFile() { using fh = new FileHandle("data.csv"); // opened // ... work ... } // fh[Symbol.dispose]() called here automatically // Async version: async function withDb() { await using db = await openDatabase(); // db[Symbol.asyncDispose]() called on exit } ``` `using` is essentially try-finally sugar. It compiles to a try-finally block that calls `[Symbol.dispose]()`. This enables safe resource management for file handles, database connections, mutexes, and temporary directories without boilerplate. Node.js 18+ supports both symbols natively.
TypeScript offers three "enum-like" options, each with distinct trade-offs: **Regular enum** — emits a runtime JavaScript object with reverse mapping for numeric enums. Safe across all build tools. ```ts enum Status { Active = "ACTIVE", Inactive = "INACTIVE" } console.log(Status.Active); // "ACTIVE" — object exists at runtime ``` **`const enum`** — values are inlined at every usage site; no object is emitted. Faster runtime, but incompatible with `isolatedModules` (Babel, esbuild, SWC) because single-file transpilers cannot perform the cross-file inlining. Also breaks `export const enum` across package boundaries in declaration files. ```ts const enum Dir { Up = 0, Down = 1 } const d = Dir.Up; // compiled to: const d = 0 ``` **String literal union** — no runtime cost, no object, works everywhere, tree-shakeable, and compatible with all tools. ```ts type Status = "ACTIVE" | "INACTIVE"; ``` Recommendation: use string literal unions for new code. Use regular enums when you need runtime iteration (`Object.values(MyEnum)`) or when working in a pure-TypeScript (tsc-only) build. Avoid `const enum` in library code or `isolatedModules` environments.
TS 4.7 introduced explicit variance annotations with `in` (contravariant) and `out` (covariant) modifiers on type parameters. This lets you declare intent explicitly and allows the compiler to skip expensive structural variance computations. ```ts interface Producer<out T> { // covariant: can only produce T get(): T; } interface Consumer<in T> { // contravariant: can only consume T set(value: T): void; } interface Transformer<in TIn, out TOut> { transform(input: TIn): TOut; } // Covariant: Producer<Dog> assignable to Producer<Animal> let dogProducer: Producer<Dog>; let animalProducer: Producer<Animal> = dogProducer; // OK ``` Variance annotations serve two purposes: documentation (they express design intent clearly) and performance (the compiler can trust the annotation rather than recomputing variance structurally, which is O(n²) for complex types). The compiler will still verify that your annotations are consistent with the type's actual usage, raising an error if they are wrong.

Frequently Asked Questions

Do TypeScript interviews require writing advanced types?

Rarely in screens, but senior rounds expect you to read advanced types comfortably — conditional, mapped, infer, and utility types come up for library authors.

What TS version should I study?

Target 5.x. Know satisfies, const type parameters, and the decorator proposal.

Is TypeScript hard after JavaScript?

The type system has a learning curve, but most of your time will be reading types rather than writing exotic ones. Start by annotating your own code and work up to generics.

Do I still need to know JavaScript?

Yes. TypeScript compiles to JavaScript — runtime behavior is 100% JS, so the event loop, prototypes, and async mechanics all still apply.

Which framework should I pair with TypeScript?

React + TS dominates frontend; NestJS and Fastify are popular on the backend. Pick the one matching your target roles.

Related Topics

Ready to apply?

TryApplyNow scores matches, tailors resumes, and tracks applications so you can focus on prep, not paperwork.

Try for free →