🔷

Top 97 TypeScript Interview Questions

Top 100 TypeScript interview questions covering static typing, interfaces, generics, decorators, advanced types, and TypeScript best practices.

97 Questions
Filter:

TypeScript is a strongly typed, compiled superset of JavaScript developed and maintained by Microsoft. It adds optional static typing, interfaces, generics, enums, and other features on top of JavaScript. TypeScript code is compiled (transpiled) to plain JavaScript that runs in any browser or Node.js environment. The key benefit is catching type errors at compile time rather than at runtime — this dramatically reduces bugs in large codebases. TypeScript is fully compatible with existing JavaScript: any valid JavaScript file is also a valid TypeScript file. It was released in 2012 and has become the industry standard for large-scale JavaScript applications, with frameworks like Angular, NestJS, and many React projects adopting it by default.

Beginner

TypeScript vs JavaScript key differences: (1) Static Typing — TypeScript has optional static types; JavaScript is dynamically typed. Types in TS are checked at compile time, preventing type-related bugs before they reach production. (2) Compilation — TypeScript must be compiled to JavaScript before it can run; JavaScript runs directly in the browser/Node. (3) Type Annotations — TS allows you to annotate variables, function parameters, and return types explicitly. (4) Advanced OOP features — TS adds interfaces, access modifiers (public/private/protected), abstract classes, and decorators. (5) Better IDE support — TypeScript enables richer autocomplete, refactoring, and error detection in editors. (6) Backwards compatibility — TypeScript can target any JavaScript version (ES5, ES6, ESNext). All JavaScript code is valid TypeScript, making migration incremental.

Beginner

TypeScript has several built-in primitive types: number (all numeric values — integers and floats), string (text), boolean (true/false), null (intentional absence), undefined (uninitialized value), symbol (unique identifiers), and bigint (large integers). Additionally: any (opt out of type checking — avoid using it), unknown (safe alternative to any — forces type narrowing before use), never (a function that never returns — throws or infinite loops), void (function returns nothing useful), object (non-primitive type). Complex types include Array<T> or T[], tuple (fixed-length arrays with specific types), enum (named constants), and object/interface types. Literal types let you constrain a value to a specific set: type Direction = "north" | "south" | "east" | "west".

Beginner

Type annotation is the syntax for explicitly specifying the type of a variable, function parameter, or return value in TypeScript using a colon followed by the type. Examples: let name: string = "Alice";, let age: number = 30;, let isActive: boolean = true;. For functions: function greet(name: string): string { return "Hello, " + name; }. Arrays: let scores: number[] = [1, 2, 3]; or let scores: Array<number> = [1, 2, 3];. Objects: let user: { name: string; age: number } = { name: "Alice", age: 30 };. TypeScript can also infer types automatically — if you write let x = 5;, TypeScript infers x: number without an annotation. You should add explicit annotations when inference is not possible or when it improves code clarity.

Beginner

Type inference is TypeScript's ability to automatically determine the type of a variable or expression without explicit annotation. When you initialize a variable at declaration, TypeScript infers its type from the assigned value: let count = 5 → TypeScript infers number. let message = "hello" → inferred as string. For functions, the return type is inferred from the return statement: function add(a: number, b: number) { return a + b; } — return type inferred as number. TypeScript also infers array element types from initial values. Inference works in most contexts — contextual typing even infers types from the surrounding context, like the type of a callback's parameters based on where it is used. Best practice: rely on inference for local variables, but always annotate function parameters and public API return types for clarity and documentation.

Beginner

The any type is an escape hatch that opts a variable out of TypeScript's type checking entirely. A variable of type any can hold any value, and you can perform any operation on it without a compile-time error — it effectively turns TypeScript into JavaScript for that variable. Example: let data: any = "hello"; data = 42; data.foo.bar.baz; — all valid with no errors, even if it crashes at runtime. When to use: migrating JavaScript codebases gradually, or working with third-party libraries that have no type definitions. Risks: defeats the purpose of TypeScript — you lose type safety, IDE autocomplete, and refactoring support. Better alternatives: use unknown when the type is truly unknown (it requires you to narrow the type before using it), use proper types or generics, or use Record<string, unknown> for unknown objects. Avoid any in production code and enable "noImplicitAny": true in tsconfig to ban implicit any.

Beginner

The unknown type is the type-safe counterpart to any. Like any, a variable of type unknown can hold any value. But unlike any, you cannot perform operations on an unknown value without first narrowing its type — TypeScript forces you to verify the type before use. Example: let value: unknown = getData();. Trying to call value.toUpperCase() is a compile error. You must narrow first: if (typeof value === "string") { value.toUpperCase(); }. Key differences: any disables type checking entirely (unsafe); unknown preserves safety by requiring narrowing. Use unknown for: function parameters that accept any type (API responses, event payloads), error handling (catch(error: unknown)), and any situation where the type is truly not known at write time but must be verified before use. Always prefer unknown over any.

Beginner

An interface in TypeScript is a contract that defines the structure (shape) of an object — the properties and methods it must have. It is a purely TypeScript construct that disappears after compilation. Syntax: interface User { name: string; age: number; email?: string; } (the ? marks optional properties). A class can implement an interface with implements: class Admin implements User { name = "Alice"; age = 30; }. Interfaces can extend other interfaces: interface Admin extends User { role: string; }. They can describe function signatures: interface Greeter { (name: string): string; } and index signatures: interface StringMap { [key: string]: string; }. Declaration merging: multiple interface declarations with the same name are automatically merged — useful for extending third-party library types. Interfaces enforce the "duck typing" philosophy of TypeScript — if an object has the required shape, it satisfies the interface regardless of how it was created.

Beginner

Both interface and type can describe object shapes, but they have key differences. Interfaces: can only describe object shapes; support declaration merging (two interfaces with the same name are merged); are extended with extends; are generally preferred for defining object/class contracts. Type aliases (type): can describe any type — primitives, unions, intersections, tuples, functions; do NOT support declaration merging; use & for intersection (composition); required for union and tuple types. Examples only possible with type: type StringOrNumber = string | number;, type Point = [number, number];, type Callback = () => void;. When to choose: use interface for object shapes in public APIs (because of declaration merging and cleaner error messages); use type for unions, intersections, mapped types, and when describing non-object types. In practice, teams often pick one and stick with it — both are valid.

Beginner

A union type allows a variable or parameter to hold one of several specified types, combined with the | operator. Example: let id: string | number;id can be either a string or a number. function formatId(id: string | number): string { return String(id); }. Union types are particularly useful with literal types to create a set of allowed values: type Status = "active" | "inactive" | "pending";. When working with a union type, TypeScript requires you to handle all possible types before using type-specific operations — this is called type narrowing. Narrow using typeof: if (typeof id === "string") { id.toUpperCase(); }, with instanceof, or with a custom type guard. Discriminated unions use a common literal property to distinguish union members — the recommended pattern for complex unions.

Beginner

An intersection type combines multiple types into one using the & operator, creating a type that has all the properties of all combined types. Example: type Employee = Person & JobInfo; — an Employee must have all properties of both Person and JobInfo. This is TypeScript's way of mixing or composing types, similar to how Object.assign() merges objects at runtime. Intersections are commonly used to combine interfaces or add extra properties: type WithTimestamps<T> = T & { createdAt: Date; updatedAt: Date; }. If two intersected types have a property with the same name but incompatible types (e.g., one has id: string, the other has id: number), the resulting property type becomes never (impossible to satisfy). Intersection types (&) create types that have more properties, while union types (|) create types with fewer guaranteed properties.

Beginner

Enums let you define a set of named constants. TypeScript supports numeric and string enums. Numeric enum: enum Direction { North, South, East, West } — values are 0, 1, 2, 3 by default (auto-incremented). Access: Direction.North === 0. You can set a custom starting value: enum Status { Active = 1, Inactive, Archived }. String enum: enum Color { Red = "RED", Green = "GREEN", Blue = "BLUE" } — more readable, preferred for serialized values and debugging. Const enum: const enum Size { Small, Medium, Large } — completely inlined at compile time, no runtime object generated, better performance. Caveats: numeric enums allow reverse lookup (Direction[0] === "North"), which can be surprising. Many TypeScript experts prefer using string literal union types (type Direction = "North" | "South") over enums for their simplicity and better tree-shaking.

Beginner

A tuple is an array with a fixed number of elements where each element has a specific type at a specific position. Unlike plain arrays, tuples enforce both the number of elements and the type at each index. Example: let point: [number, number] = [10, 20];. Mixed types: let entry: [string, number, boolean] = ["Alice", 30, true];. Access: entry[0] is typed as string, entry[1] as number. Tuples can have optional elements: [string, number?] and rest elements: [string, ...number[]]. Named tuples (TS 4.0+) improve readability: type Range = [start: number, end: number]. Common use cases: React's useState returns a tuple ([value, setter]), coordinates ([lat, lng]), key-value pairs. TypeScript cannot prevent you from pushing extra elements to a tuple via array methods — use readonly tuples (readonly [number, number]) for truly immutable tuples.

Beginner

The void type represents the absence of a return value from a function. It is used as the return type annotation for functions that do not explicitly return a value (or return undefined): function logMessage(msg: string): void { console.log(msg); }. A void function can return undefined or simply have no return statement — both are valid. void vs undefined: void is a broader concept — it signals to callers "do not expect a useful return value." A variable typed as void can only hold undefined. void in callbacks: when a function type uses void as a return type, it means the callee's return value is intentionally ignored: type Listener = (event: Event) => void — the listener can return any value, but it will be discarded. This is different from explicitly annotating a function as returning undefined.

Beginner

The never type represents values that never occur — it is the type of expressions that never complete normally. Two primary use cases: (1) Functions that never return — either because they always throw an error or run in an infinite loop: function fail(msg: string): never { throw new Error(msg); }. (2) Exhaustiveness checking — in a switch statement over a discriminated union, after handling all cases, the variable in the default case has type never. If a new union member is added without a corresponding case, the type will not be never and TypeScript will report an error — great for enforcing completeness. never is also the result of intersecting incompatible types: string & number is never. Every type is a supertype of never, meaning never can be assigned to any type. never is assignable to all types but nothing is assignable to never (except itself).

Beginner

Type narrowing is the process of refining a broader type (like a union) to a more specific type within a conditional block. TypeScript uses control flow analysis to track which types are possible at each point in the code. Common narrowing techniques: typeof guard: if (typeof value === "string") { value.toUpperCase(); }. instanceof guard: if (error instanceof TypeError) { ... }. Truthiness narrowing: if (value) { /* value is not null/undefined/falsy */ }. Equality narrowing: if (x === "hello") { /* x is "hello" */ }. in operator: if ("swim" in animal) { animal.swim(); }. Discriminated unions: a common literal property used to differentiate union members. Custom type guards: functions with a value is Type return type. After narrowing, TypeScript knows the exact type and provides full type checking and autocomplete for that specific type's members.

Beginner

A type guard is an expression or function that narrows the type of a variable within a conditional block. Built-in type guards: typeof (checks primitive types), instanceof (checks class instances), in (checks property existence), truthiness checks. User-defined type guard: a function with a return type of parameterName is Type: function isString(value: unknown): value is string { return typeof value === "string"; }. After calling this guard in a condition, TypeScript knows the type within that block: if (isString(data)) { data.toUpperCase(); /* data is string here */ }. Assertion functions (TS 3.7): function assertIsString(val: unknown): asserts val is string { if (typeof val !== "string") throw new Error(); } — after calling this, the type is narrowed without an if block. Type guards are essential for working safely with unknown types, union types, and API responses.

Beginner

Optional properties in interfaces and type aliases are marked with a ? after the property name: interface User { name: string; email?: string; }email may or may not be present. When accessing optional properties, TypeScript requires checking for undefined first (unless using optional chaining user.email?.toUpperCase()). Optional function parameters are also marked with ?: function greet(name: string, greeting?: string): string { return (greeting ?? "Hello") + ", " + name; }. Optional parameters must come after required parameters. Default parameters are similar but provide a fallback value: function greet(name: string, greeting = "Hello"): string { ... } — TypeScript infers the parameter is optional and of the default value's type. The difference: optional parameters can be explicitly passed as undefined; default parameters also handle undefined by substituting the default.

Beginner

The readonly modifier prevents a property from being changed after it is initialized. It can be applied to interface properties, class properties, and tuple elements. Interface: interface Point { readonly x: number; readonly y: number; } — you can create a Point but cannot reassign x or y afterwards. Class: class Circle { readonly radius: number; constructor(r: number) { this.radius = r; } }radius can only be set in the constructor. Readonly arrays: const arr: readonly number[] = [1, 2, 3]; — prevents push, pop, and reassignment. Readonly utility type: Readonly<T> makes all properties of T readonly: type ReadonlyUser = Readonly<User>. Note: readonly is a compile-time-only check — it does not create a frozen object at runtime. For deep immutability at runtime, use Object.freeze() or libraries like Immer. Use readonly liberally — it communicates intent and prevents accidental mutations.

Beginner

A type assertion is a way to tell the TypeScript compiler "I know the type of this value better than you do." It does NOT perform any runtime type conversion — it is purely a compile-time instruction. Two syntaxes: value as Type (preferred, works in JSX) and <Type>value (older syntax, does not work in TSX files). Example: const input = document.getElementById("name") as HTMLInputElement; — TypeScript knows getElementById returns HTMLElement | null, but you assert it is specifically an HTMLInputElement to access .value. Non-null assertion: value! asserts that a value is not null or undefined. Use type assertions sparingly — if you are wrong about the type, you will get runtime errors with no compile-time warning. Prefer type guards and proper typing over assertions. Double assertion: if TypeScript rejects a direct assertion, you can bridge through unknown: value as unknown as TargetType — a strong signal that something suspicious is happening.

Beginner

TypeScript offers two syntaxes for type assertions that are functionally identical in most cases: Angle bracket syntax: let len = (<string>someValue).length; — the older form from early TypeScript. as syntax: let len = (someValue as string).length; — the modern, preferred form. The critical difference is in JSX/TSX files: angle bracket syntax conflicts with JSX element syntax (which also uses angle brackets), so it cannot be used in .tsx files. The as keyword is unambiguous and works everywhere. For this reason and for consistency, the TypeScript community has standardized on the as syntax. TypeScript's official documentation and most style guides recommend using as exclusively. Both syntaxes generate identical compiled JavaScript output (which is nothing — assertions are erased at compile time).

Beginner

TypeScript classes extend JavaScript ES6 classes with additional features for type safety. A basic TypeScript class: class Animal { name: string; constructor(name: string) { this.name = name; } speak(): void { console.log(this.name + " makes a sound."); } }. TypeScript adds: Access modifierspublic (default, accessible anywhere), private (only within the class), protected (within class and subclasses). readonly properties. Parameter properties — shorthand for declaring and initializing in the constructor: constructor(private name: string, public age: number) {}. Abstract classes — cannot be instantiated, serve as base classes. Implements — a class can implement multiple interfaces. Static members — belong to the class itself, not instances. TypeScript classes support full inheritance with extends. TypeScript also supports private class fields using the # syntax (JavaScript standard) which provide true runtime privacy, unlike TypeScript's private which is only a compile-time check.

Beginner

TypeScript provides three access modifiers to control the visibility of class members. public (default): the member is accessible from anywhere — inside the class, outside the class, and in subclasses. Explicitly writing public is optional but improves clarity. private: the member is only accessible inside the class where it is declared. Subclasses cannot access it. Example: class BankAccount { private balance: number = 0; }. Important: TypeScript's private is a compile-time only restriction — the property still exists and is accessible at runtime via JavaScript. For true runtime privacy, use JavaScript private fields (#balance). protected: accessible inside the class and in any class that extends it (subclasses), but not from outside the class hierarchy. Useful for template method patterns where base classes share implementation details with subclasses. Access modifiers exist only in TypeScript — they are completely erased after compilation to JavaScript.

Beginner

An abstract class is a class that cannot be instantiated directly — it is designed to be subclassed. Declared with the abstract keyword: abstract class Shape { abstract area(): number; printArea(): void { console.log("Area:", this.area()); } }. Abstract classes can contain abstract methods (declared but not implemented — subclasses must implement them) and concrete methods (fully implemented and inherited). A class that extends an abstract class must implement all abstract methods, or itself be declared abstract. new Shape() is a compile-time error. Use abstract classes when you want to define a common interface and shared behavior for a family of related classes, while forcing subclasses to provide specific implementations. Abstract vs Interface: abstract classes can have constructor logic, concrete method implementations, access modifiers, and state — interfaces are purely structural contracts without any implementation.

Beginner

extends is used for inheritance — a class or interface inheriting from another class or interface. A class that extends another class inherits all its properties and methods. A child class can override parent methods with super for calling the parent implementation. TypeScript only supports single class inheritance (one extends). implements is used to declare that a class satisfies the contract of one or more interfaces or abstract classes, without inheriting their implementation. A class can implements multiple interfaces (comma-separated). implements only performs a compile-time shape check — it does not inherit any code. Example: class Dog extends Animal implements Pet, Trainable { ... }. Key differences: extends gives you the implementation (actual code); implements only guarantees the shape (structural contract). Interfaces can also extend other interfaces (even multiple ones): interface Admin extends User, Logger { ... }.

Beginner

The tsconfig.json is the configuration file for the TypeScript compiler. It is placed in the project root and controls how TypeScript compiles your code. Key compiler options: "target" — which JavaScript version to compile to (e.g., "ES5", "ES2020", "ESNext"). "module" — module system ("commonjs" for Node, "ESNext" for bundlers). "strict" — enables all strict type checks (highly recommended). "outDir" — where to put compiled JavaScript files. "rootDir" — root of source files. "include"/"exclude" — file patterns to compile or skip. "lib" — which built-in type definitions to include (e.g., "DOM", "ES2022"). "sourceMap": true — generates source maps for debugging. "noEmit": true — type-check only without generating files (used with bundlers). "paths" — module path aliases. "baseUrl" — base for non-relative imports. Use tsc --init to generate a starter tsconfig.json with all options commented.

Beginner

Setting "strict": true in tsconfig.json enables a collection of strict type-checking options that significantly improve type safety. It enables these sub-options: noImplicitAny — error when TypeScript infers any because a type cannot be determined. strictNullChecksnull and undefined are no longer assignable to other types unless explicitly included; you must handle them. strictFunctionTypes — stricter checking of function parameter types (contravariance). strictBindCallApply — type-safe bind, call, and apply on functions. strictPropertyInitialization — class properties must be assigned in the constructor. noImplicitThis — error when this has an implicit any type. alwaysStrict — emits "use strict" in all output files. Best practice: always enable "strict": true from the start of a project. Retrofitting strict mode onto an existing codebase is painful — do it early.

Beginner

Function overloads allow you to define multiple call signatures for a single function, letting it accept different argument types and return different types based on what is passed. You declare the overload signatures first (without body), then implement with a single implementation signature (with body): function format(value: string): string; function format(value: number): string; function format(value: string | number): string { return String(value); }. TypeScript uses the overload signatures for type checking (not the implementation signature) — callers see only the declared overloads. Overloads are resolved in order — TypeScript picks the first matching signature. The implementation signature must be compatible with all overload signatures. Use overloads when a function's return type or behavior genuinely depends on the argument types. For simpler cases, union types and optional parameters are cleaner. Overloads also work on class methods and interface methods.

Beginner

Default parameter values in TypeScript work the same as JavaScript's ES6 default parameters but with type inference. When you provide a default value, TypeScript automatically infers the parameter type from the default: function greet(name: string, greeting = "Hello"): string { return `\${greeting}, \${name}!`; }greeting is inferred as string. The parameter becomes optional from the caller's perspective — you can omit it or pass undefined to use the default. You can combine with explicit types: function setPage(page: number = 1, limit: number = 10) { ... }. Default parameters can reference earlier parameters: function createRange(start: number, end: number = start + 10) { ... }. Unlike optional parameters (?), default parameters do not result in the type being widened to include undefined within the function body — they are guaranteed to have a value.

Beginner

The keyof operator creates a union type of all the keys (property names) of a given type. For interface User { name: string; age: number; email: string; }, keyof User produces "name" | "age" | "email". This is useful for creating type-safe functions that access object properties by key: function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key]; }. This ensures that only valid keys of the object can be passed, and the return type is correctly inferred as the type of that property. Without keyof, you would need to use string for the key and any for the return — losing all type safety. keyof works with index signatures: keyof { [key: string]: number } is string | number (because numeric indices are valid in JavaScript). keyof any is string | number | symbol.

Beginner

In TypeScript, typeof has two roles. At runtime (JavaScript): it returns a string representing the type of a value ("string", "number", "boolean", "object", "function", "undefined", "symbol", "bigint"). Used for type narrowing: if (typeof x === "string") { x.toUpperCase(); }. At type level (TypeScript-specific): you can use typeof in a type position to extract the type of a variable or expression: const config = { port: 3000, host: "localhost" }; type Config = typeof config; — TypeScript infers Config as { port: number; host: string }. This is especially useful for inferring types from complex objects, arrays, and function return types without manually writing them. Combined with ReturnType<typeof fn> to get a function's return type, or with keyof typeof obj to get the keys of a value as a type.

Beginner

While type and interface overlap significantly for object types, their differences matter in practice. Similarities: both can describe object shapes, be extended/combined, and are erased at runtime. Differences: (1) Declaration merging — interfaces with the same name are automatically merged; type aliases cannot be re-declared. (2) What they can describe — type aliases can describe any type (unions, intersections, primitives, tuples, functions); interfaces can only describe object/class shapes and function signatures. (3) Extension syntax — interfaces use extends; type aliases use & for intersection. (4) Error messages — interfaces often produce more readable error messages in the compiler output. (5) Performance — interfaces are generally faster to check in complex codebases because TypeScript caches them. Community consensus: use interface for object shapes in public APIs; use type for unions, intersections, mapped types, and when you need the flexibility of a type alias.

Beginner

Literal types restrict a type to a specific set of exact values, rather than any value of that base type. String literals: type Direction = "North" | "South" | "East" | "West"; — only these four strings are valid, not any arbitrary string. Numeric literals: type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;. Boolean literals: type Yes = true;. You can mix literal types in a union: type InputMode = "text" | "password" | 0 | false;. Literal types are crucial for discriminated unions — objects with a common literal property used to distinguish variants: type Shape = { kind: "circle"; radius: number } | { kind: "rect"; width: number; height: number };. TypeScript narrows the type when you check shape.kind. Const assertions (as const) turn widened types into literal types: const colors = ["red", "green", "blue"] as const; — type is readonly ["red", "green", "blue"], not string[].

Beginner

The as const assertion (const assertion) tells TypeScript to treat an expression as a deeply immutable literal type rather than widening it to a general type. Without as const: const config = { port: 3000 } — TypeScript infers { port: number }. With as const: const config = { port: 3000 } as const — TypeScript infers { readonly port: 3000 } (the literal value 3000, not the general type number). For arrays: const directions = ["North", "South"] as const — type is readonly ["North", "South"], not string[]. This is powerful for creating type-safe enums without the enum keyword: const STATUS = { Active: "active", Inactive: "inactive" } as const; type Status = typeof STATUS[keyof typeof STATUS]; — gives you "active" | "inactive". as const makes all nested properties readonly and infers the most specific types possible.

Beginner

Optional chaining (?.) is a JavaScript/TypeScript operator that safely accesses nested properties or calls methods without throwing an error if an intermediate value is null or undefined. Instead of an error, it short-circuits and returns undefined. Property access: user?.address?.street — returns undefined if user or address is nullish. Method call: obj?.method?.(args) — only calls the method if obj and method are not nullish. Array access: arr?.[0]. TypeScript understands optional chaining and correctly narrows types after the ?. — it knows the result type includes undefined. Combine with nullish coalescing for defaults: const city = user?.address?.city ?? "Unknown". Without optional chaining, you would write: user && user.address && user.address.street — much more verbose. Optional chaining was introduced in TypeScript 3.7 and compiles down to compatibility checks for older targets.

Beginner

The nullish coalescing operator (??) returns the right-hand side operand when the left-hand side is null or undefined — and only then. It is different from the logical OR (||) which also triggers for any falsy value (like 0, "", false). Example: const port = userConfig.port ?? 3000 — if port is null or undefined, use 3000; but if it is 0 (a valid port), keep 0. With ||, 0 would be treated as falsy and replaced with 3000 — a common bug. Nullish assignment (??=): user.name ??= "Anonymous" — assigns only if name is null/undefined. Combine with optional chaining: const name = user?.profile?.displayName ?? "Guest" — clean and safe default value. TypeScript infers the result type correctly, excluding null and undefined from the result when the right-hand side is a non-nullish type.

Beginner

Generics allow you to write reusable code that works with a variety of types while maintaining full type safety. Instead of using any, you use a type parameter (a placeholder for a specific type). Syntax: function identity<T>(arg: T): T { return arg; }T is the type parameter. When you call identity("hello"), TypeScript infers T = string, and the return type is string. You can explicitly specify: identity<number>(42). Generic interfaces: interface Box<T> { value: T; }. Generic classes: class Stack<T> { private items: T[] = []; push(item: T): void { ... } }. You can have multiple type parameters: function pair<K, V>(key: K, value: V): [K, V] { return [key, value]; }. Generics are the foundation of TypeScript's type system — they power all utility types (Array, Promise, Map, Set, etc.) and enable writing type-safe libraries.

Beginner

Generic constraints restrict what types can be used as type arguments using the extends keyword. Without constraints, a generic type parameter can be any type. With a constraint, you specify a minimum structure the type must have: function getLength<T extends { length: number }>(arg: T): number { return arg.length; }T must have a length property; this accepts strings, arrays, and any other type with length, but not numbers. Extend an interface: function processUser<T extends User>(user: T): T { ... }. Use keyof constraint for type-safe property access: function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key]; }. Constraints can reference other type parameters: function copyProps<S, D extends S>(source: S, dest: D): D { ... }. Constraints let you write flexible, reusable code while still accessing specific members of the generic type safely.

Beginner

TypeScript ships with built-in utility types that perform common type transformations. Key ones: Partial<T> — makes all properties of T optional (useful for update operations). Required<T> — makes all properties required (opposite of Partial). Readonly<T> — makes all properties readonly. Record<K, V> — creates an object type with keys of type K and values of type V. Pick<T, K> — creates a type with only the specified properties K from T. Omit<T, K> — creates a type excluding the specified properties K from T. Exclude<T, U> — removes types from T that are assignable to U. Extract<T, U> — keeps only types from T that are assignable to U. NonNullable<T> — removes null and undefined from T. ReturnType<T> — extracts the return type of a function type. Parameters<T> — extracts parameter types as a tuple. These utility types are built using TypeScript's advanced type features (mapped types, conditional types) and are essential tools for type manipulation.

Beginner

Partial<T> takes a type T and creates a new type where every property is optional (marked with ?). This is useful for update operations where you may only want to change some fields: type UpdateUser = Partial<User>; — all properties of User become optional. Implementation: type Partial<T> = { [K in keyof T]?: T[K] };. Required<T> is the opposite — it removes the ? from all properties, making every optional property required: type CompleteUser = Required<User>;. Useful when you want to assert that all fields are present. Implementation: type Required<T> = { [K in keyof T]-?: T[K] }; — the -? removes optionality. Common use case for Partial: function updateUser(id: string, changes: Partial<User>): User { ... } — the caller only needs to provide the fields they want to change, not the full user object.

Beginner

TypeScript uses JavaScript's module system with enhanced type support. A file is a module if it has at least one top-level import or export statement; otherwise it is a script (global scope). Exports: export const PI = 3.14;, export function greet() {}, export interface User {}, export default class App {}. Imports: import { PI, greet } from "./utils";, import App from "./App";, import * as Utils from "./utils";. TypeScript also supports type-only imports/exports (TS 3.8+): import type { User } from "./types"; — these are completely erased at compile time, which helps bundlers optimize tree-shaking. The module option in tsconfig controls the module format (commonjs, es2020, node16, etc.). TypeScript understands declaration files (.d.ts) that provide type information for JavaScript packages without rewriting them.

Beginner

Declaration merging is TypeScript's behavior of combining multiple declarations with the same name into a single definition. It happens automatically when the TypeScript compiler encounters two or more declarations with the same name in the same scope. Interface merging: the most common case — declaring an interface twice merges the properties: interface Window { myProp: string; } interface Window { anotherProp: number; } — results in Window having both properties. This is how libraries (like @types/express) extend third-party interfaces. Namespace merging: namespaces with the same name are merged. Merging a namespace with a class/function/enum: allows adding static members to classes or attaching types to functions. Declaration merging does NOT work with type aliases — re-declaring a type alias with the same name is an error. This is a key reason to prefer interface for things that may need to be extended externally (like library types).

Beginner

Mapped types create new types by transforming all properties of an existing type using the [K in keyof T] syntax. They iterate over each key of type T and produce a new property. Basic example: type Optional<T> = { [K in keyof T]?: T[K] }; — makes all properties optional (same as Partial<T>). Making all properties readonly: type Frozen<T> = { readonly [K in keyof T]: T[K] };. Nullify all values: type Nullable<T> = { [K in keyof T]: T[K] | null };. Modifiers: use + or - to add or remove modifiers — -readonly removes readonly, -? removes optionality. Key remapping (TS 4.1): type Getters<T> = { [K in keyof T as `get\${Capitalize<string & K>}`]: () => T[K] }; — generates getter method types. Mapped types are the foundation of most built-in utility types (Partial, Required, Readonly, Record, Pick, Omit) and enable powerful type transformations.

Intermediate

Conditional types allow types to vary based on a condition, using a ternary-like syntax: T extends U ? X : Y — "if T is assignable to U, the type is X; otherwise, it is Y." Example: type IsString<T> = T extends string ? "yes" : "no";. Combined with generics, conditional types become very powerful: type NonNullable<T> = T extends null | undefined ? never : T. Distributive conditional types: when the checked type is a naked type parameter, the condition distributes over union members: IsString<string | number> becomes IsString<string> | IsString<number> which is "yes" | "no". Wrap in a tuple to prevent distribution: [T] extends [U]. Infer: the infer keyword within conditional types lets you capture a type into a variable: type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never. Conditional types power many advanced utility types.

Intermediate

The infer keyword is used within conditional types to introduce and capture a type variable that TypeScript deduces from the structure of the type being checked. It can only appear on the right side of an extends clause in a conditional type. Classic example — extracting a function's return type: type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;. TypeScript fills in R with whatever the actual return type is. Extracting the first parameter: type FirstParam<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;. Unwrapping a Promise: type Awaited<T> = T extends Promise<infer U> ? U : T; (simplified). Extracting array element type: type ElementType<T> = T extends (infer E)[] ? E : never;. infer enables powerful type introspection — extracting parts of complex types without knowing them in advance. It is how TypeScript's built-in ReturnType, Parameters, InstanceType, and Awaited utility types are implemented.

Intermediate

Template literal types (TS 4.1) bring JavaScript template literal syntax into the type system, allowing string literal types to be combined and transformed. Basic example: type EventName<T extends string> = `on\${Capitalize<T>}`;EventName<"click"> becomes "onClick". Union distribution: type Directions = "top" | "right" | "bottom" | "left"; type CSSProp = `margin-\${Directions}`; — produces "margin-top" | "margin-right" | "margin-bottom" | "margin-left". TypeScript also provides string manipulation utility types: Uppercase<S>, Lowercase<S>, Capitalize<S>, Uncapitalize<S>. Template literal types are used for: type-safe event names, CSS property names, API endpoint paths, i18n keys, and strongly typed string manipulation. Combined with mapped types and infer, they enable extremely precise typing of string-based APIs.

Intermediate

Decorators are a stage-3 JavaScript proposal (and TypeScript feature) that allows adding metadata and modifying the behavior of classes, methods, properties, and parameters using a special syntax with @. Enable them with "experimentalDecorators": true in tsconfig. Class decorator: @sealed class MyClass { ... } — receives the class constructor, can modify or replace it. Method decorator: @log myMethod() { ... } — receives the target, method name, and property descriptor. Property decorator: @validate myProp: string. Parameter decorator: myMethod(@required param: string) { ... }. Decorators are heavily used in frameworks: Angular uses them for components (@Component), services (@Injectable); NestJS uses them for controllers (@Controller), guards, and middleware; TypeORM uses them for entity mapping (@Entity, @Column). They enable meta-programming patterns like AOP (aspect-oriented programming) for logging, validation, and authorization.

Intermediate

ReturnType<T> is a built-in utility type that extracts the return type of a function type T. It is implemented using conditional types and infer: type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any. Usage: function getUserName(): string { return "Alice"; } type NameType = ReturnType<typeof getUserName>;NameType is string. This is powerful when you want to derive types from existing functions rather than writing them manually — the type is always in sync with the function. For async functions, it returns Promise<T>; combine with Awaited to unwrap: type Resolved = Awaited<ReturnType<typeof asyncFn>>. ReturnType is particularly useful for functions that return complex objects — infer the type from the function rather than duplicating the type definition.

Intermediate

Parameters<T> is a built-in utility type that extracts the parameter types of a function type T as a tuple. Implementation: type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never. Example: function createUser(name: string, age: number, admin: boolean): User { ... } type CreateUserArgs = Parameters<typeof createUser>;CreateUserArgs is [name: string, age: number, admin: boolean]. Useful for: wrapper functions that need the same parameters as another function, currying helpers, memoization wrappers. Access individual parameters: type FirstArg = Parameters<typeof createUser>[0]; — gives string. Related: ConstructorParameters<T> does the same for class constructor parameters, returning the types as a tuple. These types help you stay DRY — define parameter types once in the function and derive them everywhere else.

Intermediate

Record<K, V> creates an object type where the keys are of type K and the values are of type V. Implementation: type Record<K extends keyof any, V> = { [P in K]: V }. Simple usage: type StringToNumber = Record<string, number>; — an object with any string keys and number values. With literal key types: type UserRoles = Record<"admin" | "editor" | "viewer", boolean>; — exactly these three keys, all boolean values. Common use case — mapping an enum to values: type PageMeta = Record<Routes, { title: string; description: string }>. Record is essentially a cleaner alternative to an index signature ({ [key: string]: V }) when the keys are known. Unlike index signatures, Record with literal keys enforces that all specified keys are present. Combine with Partial for optional keys: Partial<Record<K, V>>.

Intermediate

Pick<T, K> creates a new type by selecting only the properties K from type T. K must be a key (or union of keys) of T. Implementation: type Pick<T, K extends keyof T> = { [P in K]: T[P] }. Example: interface User { id: number; name: string; email: string; password: string; } type PublicUser = Pick<User, "id" | "name" | "email">;PublicUser has only these three properties, excluding password. Useful for: API response shapes (pick only safe-to-expose fields), form data types (pick only editable fields), derived types that need a subset of a larger type. Compare with Omit: Pick keeps the specified keys; Omit removes the specified keys. Use Pick when the subset is small and explicit; use Omit when excluding a small number of properties from a large type. Both create new types without mutating the original.

Intermediate

Omit<T, K> creates a new type by removing the properties K from type T — the opposite of Pick. Implementation: type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>. Example: type CreateUserDto = Omit<User, "id" | "createdAt" | "updatedAt">; — the DTO type has all User properties except the server-generated ones. Another example: type UpdateUserDto = Partial<Omit<User, "id">>; — all User fields except id, all optional. Omit is ideal when you have a large type and only want to exclude a small number of properties. Key difference from Pick: Omit<T, K> where K does not extend keyof T will not produce an error (unlike Pick which is stricter about K extending keyof T). Combining: Omit<T, K1> & Pick<T, K2> for complex selections. Omit is extremely common in TypeScript codebases for creating form types, DTOs, and API shapes.

Intermediate

Index signatures describe types that can have any number of properties with a consistent key and value type. Syntax: interface StringMap { [key: string]: string; } — any string key maps to a string value. Numeric index: interface NumberArray { [index: number]: string; }. Index signatures can be combined with specific properties, but specific property types must be assignable to the index signature value type: interface Config { [key: string]: string | number; version: number; }. Access: const val = map["anyKey"] — TypeScript knows val is string. noUncheckedIndexedAccess option in tsconfig makes TypeScript include undefined in the type of indexed access (since the key might not exist): val becomes string | undefined — safer but more verbose. Prefer Map for runtime key-value storage; use index signatures for describing existing JavaScript patterns. Record<string, V> is a cleaner alternative for typed dictionaries.

Intermediate

Exclude<T, U> removes from union type T all members that are assignable to U. Implementation: type Exclude<T, U> = T extends U ? never : T — it distributes over the union and replaces matching members with never, which then collapses. Example: type Colors = "red" | "blue" | "green"; type NotBlue = Exclude<Colors, "blue">; — result is "red" | "green". Exclude any object type: type Primitives = Exclude<string | number | boolean | object, object>; — result is string | number | boolean. Compare with Omit: Exclude works on union types (removes union members); Omit works on object types (removes properties). Related: Extract<T, U> is the opposite — keeps only the members of T that ARE assignable to U. Use case: type EventKeys = Exclude<keyof HTMLElement, "style" | "className"> to create a subset of DOM event keys.

Intermediate

Extract<T, U> keeps from union type T only the members that are assignable to U — the opposite of Exclude. Implementation: type Extract<T, U> = T extends U ? T : never. Example: type StringsOnly = Extract<string | number | boolean, string | number>; — result is string | number. Finding matching object types: type Shapes = { kind: "circle" } | { kind: "rect" } | { kind: "triangle" }; type Circles = Extract<Shapes, { kind: "circle" }>; — result is { kind: "circle" }. Useful for filtering union types to only include members matching a certain pattern. Combined with Record and keyof, you can filter object keys by value type: type MethodKeys<T> = { [K in keyof T]: T[K] extends Function ? K : never }[keyof T]; — extracts only keys whose values are functions. Extract and Exclude form a complementary pair for union type manipulation.

Intermediate

NonNullable<T> removes null and undefined from a type. Implementation: type NonNullable<T> = T & {} or equivalently type NonNullable<T> = T extends null | undefined ? never : T. Example: type MaybeString = string | null | undefined; type DefiniteString = NonNullable<MaybeString>; — result is string. Useful when you have received a value that TypeScript considers nullable but you have verified it is not null (perhaps via an assertion or runtime check). Often used with function return types: type UserOrNothing = User | null | undefined; type GuaranteedUser = NonNullable<UserOrNothing>;. In strict mode with strictNullChecks, you frequently need to remove nullability after runtime checks. Compare with the non-null assertion (!): NonNullable is a type-level transformation; the ! operator is an expression-level assertion that tells the compiler "this specific value is not null."

Intermediate

A namespace is a TypeScript-specific way to organize code under a named scope, preventing naming collisions in the global space. Syntax: namespace Validation { export interface StringValidator { isAcceptable(s: string): boolean; } export class LettersOnlyValidator implements StringValidator { ... } }. Access: const validator = new Validation.LettersOnlyValidator();. Namespaces can be nested and split across multiple files. Before ES6 modules, namespaces (formerly "internal modules") were the primary way to organize TypeScript code. Namespaces vs Modules: today, ES module syntax (import/export) is strongly preferred for organizing code — namespaces are considered legacy for new code. However, namespaces are still useful for: (1) ambient type declarations in .d.ts files, (2) global augmentation, (3) organizing types without creating separate files. Declaration merging also works with namespaces, allowing you to merge namespaces with existing classes or functions.

Intermediate

Ambient declarations tell the TypeScript compiler about the shape of JavaScript code that exists at runtime but was not written in TypeScript. They use the declare keyword without providing an implementation. declare var __VERSION__: string; — tells TS that a global variable exists. declare function require(module: string): any;. declare class Animal { name: string; }. declare module "lodash" { export function chunk<T>(array: T[], size: number): T[][]; }. Ambient declarations are placed in .d.ts declaration files (type definition files). They have no runtime equivalent — the compiler uses them for type checking but emits nothing. The @types organization on npm publishes community-maintained declaration files for popular JavaScript libraries (@types/react, @types/node, etc.). declare global { interface Window { myProp: string; } } augments the global scope. The typeRoots and types tsconfig options control which type declarations are included.

Intermediate

A declaration file (.d.ts) contains only type information — no implementation code. It describes the shape of JavaScript APIs to TypeScript without rewriting them in TypeScript. Declaration files consist entirely of ambient declarations: declare statements for variables, functions, classes, interfaces, and modules. They are: (1) Generated automatically by the TypeScript compiler when building a library with "declaration": true in tsconfig — alongside the compiled .js output. (2) Hand-written for existing JavaScript libraries. (3) Published to npm as @types/libraryname packages (DefinitelyTyped). When you install @types/react, TypeScript finds the .d.ts files and knows the types of all React APIs. Triple-slash directives (/// <reference types="node" />) reference type packages. .d.ts files enable the TypeScript ecosystem to type-check code that uses any JavaScript library, regardless of whether it was originally written in TypeScript.

Intermediate

import type (TypeScript 3.8+) imports only the type information from a module — it is guaranteed to be completely erased from the compiled JavaScript output, leaving no trace. Regular import may or may not be erased depending on whether the imported values are used as values (not just types). Why it matters: (1) Performance: type-only imports allow bundlers and compilers to more easily eliminate unused imports. (2) Circular references: type imports avoid runtime circular dependency issues, since they exist only at type-check time. (3) Clarity: signals to readers that this import is used only for typing, not for runtime behavior. (4) With "isolatedModules": true (required for Babel/SWC transpilation), TypeScript enforces that re-exported types use export type. Inline type imports (TS 4.5): import { type User, fetchUser } from "./api" — mix type and value imports in one statement. Best practice: use import type whenever importing only for typing purposes.

Intermediate

The satisfies operator (TypeScript 4.9) validates that a value matches a type without widening the inferred type of the value. The problem it solves: when you annotate a variable with a type (const colors: Record<string, string | RGB> = { red: "#f00", ... }), TypeScript uses the annotated type everywhere — you lose the specific inferred type of each value. With as assertion, you lose type checking. With satisfies: const colors = { red: "#f00", blue: [0, 0, 255] } satisfies Record<string, string | RGB>; — TypeScript validates the object matches the type (type error if it does not), but still infers the most specific type for each property. colors.red is typed as string (not string | RGB), and colors.blue is typed as number[]. This gives you both type safety (validation) and specificity (narrow inferred type) — the best of both worlds.

Intermediate

A discriminated union (also called tagged union or algebraic data type) is a pattern where a union's members each have a common literal property (the discriminant) that TypeScript uses to narrow the type. Example: type Shape = | { kind: "circle"; radius: number } | { kind: "rectangle"; width: number; height: number } | { kind: "triangle"; base: number; height: number };. The kind property is the discriminant. When you check shape.kind, TypeScript narrows the type to the matching member: switch (shape.kind) { case "circle": return Math.PI * shape.radius ** 2; case "rectangle": return shape.width * shape.height; }. If you add a new variant to the union but forget to handle it in the switch, TypeScript flags the error (if you add a default case that assigns to never). Discriminated unions are a core pattern for modeling complex state in TypeScript — error/success responses, UI states (loading/loaded/error), form validation states, etc.

Intermediate

Generic default types (TypeScript 2.3) allow you to specify a default type for a generic type parameter, similar to default parameter values in functions. Syntax: interface Container<T = string> { value: T; } — if T is not specified, it defaults to string. So Container is equivalent to Container<string>. Another example: function createArray<T = number>(length: number, fill: T): T[] { return Array(length).fill(fill); }. Defaults can reference earlier type parameters: type EventHandler<E extends Event = Event> = (event: E) => void;. Generic defaults are required to come after non-default type parameters. Use cases: library APIs where a sensible default exists but should be overridable (React's Component<P = {}, S = {}>), creating flexible yet convenient generic types. When you have multiple type parameters, trailing ones can have defaults even if leading ones do not.

Intermediate

Module augmentation lets you add new declarations to an existing module or library without modifying its source code. This is how you extend third-party library types. To augment a module, import it and re-declare it in a declare module block: declare module "express" { interface Request { user?: User; currentTenant?: Tenant; } } — now Express's Request type includes your custom properties. This is typically placed in a .d.ts file (e.g., types/express.d.ts) and included in tsconfig. Global augmentation: extend the global scope by wrapping in declare global { ... } inside a module file. Module augmentation uses declaration merging internally — TypeScript merges the augmented declarations with the original module's declarations. Common use cases: adding custom properties to Express Request, extending Window, augmenting Vuex store types, adding methods to built-in prototypes. Module augmentation works only with ES module syntax — ambient modules use a different pattern.

Intermediate

InstanceType<T> extracts the instance type of a constructor function or class. It takes a class constructor type and returns the type of the instances that constructor creates. Implementation: type InstanceType<T extends abstract new (...args: any) => any> = T extends abstract new (...args: any) => infer R ? R : any. Usage: class User { name: string; constructor(name: string) { this.name = name; } } type UserInstance = InstanceType<typeof User>;UserInstance is the same as the User type (the instance type). Most useful when working with class references passed as parameters or stored in variables: function createInstance<T extends new (...args: any) => any>(ctor: T): InstanceType<T> { return new ctor(); } — this factory function returns the correct instance type regardless of which class is passed. Related: ConstructorParameters<T> extracts the constructor's parameter types.

Intermediate

Awaited<T> (TypeScript 4.5) recursively unwraps Promise types to get the resolved value type. It handles nested promises and thenables. Example: type A = Awaited<Promise<string>>; — result is string. Nested: type B = Awaited<Promise<Promise<number>>>; — result is number. This type replaces older patterns like ReturnType<typeof fn> extends Promise<infer T> ? T : never. Common use case: type ApiResult = Awaited<ReturnType<typeof fetchUser>>; — gets the actual resolved type of an async function's return. Before Awaited, getting the type of an awaited async function result required verbose conditional type expressions. Awaited is used internally in TypeScript's type for Promise.all() — it can correctly type the resolved array even with mixed promise and non-promise values in the input tuple.

Intermediate

Variance describes how subtyping relationships between complex types relate to subtyping of their component types. In TypeScript: Covariance means "subtype in, subtype out" — if Cat is a subtype of Animal, then Array<Cat> is a subtype of Array<Animal> (you can use a Cat array where an Animal array is expected). Most positions in TypeScript are covariant. Contravariance means the direction is reversed — if Cat extends Animal, a function (a: Animal) => void is a subtype of (c: Cat) => void. This is because a function that can handle any Animal can certainly handle a Cat, but not vice versa. TypeScript enforces contravariance for function parameters with "strictFunctionTypes": true. This matters for callbacks and event handlers — a more general callback is safely substitutable for a more specific one. Methods (unlike function properties) are bivariant in TypeScript for historical compatibility reasons.

Intermediate

A type predicate is the return type annotation paramName is Type used to create user-defined type guards — functions that tell TypeScript what type a value is after the function returns true. Syntax: function isString(value: unknown): value is string { return typeof value === "string"; }. Usage: if (isString(data)) { data.toUpperCase(); /* TypeScript knows data is string here */ }. Without the predicate (returning just boolean), TypeScript would not narrow the type inside the if-block. Type predicates can narrow to any type: function isUser(obj: unknown): obj is User { return typeof obj === "object" && obj !== null && "name" in obj; }. Assertion functions are related but use asserts value is Type — instead of wrapping in an if-block, assertion functions throw if the condition fails, and the type is narrowed after the call site. Use type predicates for complex runtime type checks that TypeScript's built-in narrowing cannot express.

Intermediate

Type widening is TypeScript's behavior of inferring a broader type than the literal value when a variable is mutable. When you write let x = "hello", TypeScript widens the type to string (not the literal "hello"), because let allows reassignment. With const x = "hello", TypeScript keeps it as the literal type "hello" because const cannot be reassigned. Widening also happens with null — in some contexts, TypeScript widens null to string | null. You can prevent widening with as const: const colors = ["red", "green"] as const keeps it as a readonly tuple. Type narrowing is the opposite — starting from a broad type (like a union) and using type guards, checks, and control flow to refine it to a more specific type within a block of code. Narrowing goes from wide to specific; widening goes from specific to wide. TypeScript's control flow analysis automatically performs narrowing based on conditionals, assignments, and runtime checks.

Intermediate

TypeScript provides a special this type that represents the type of the current object in methods and functions, resolved polymorphically. Polymorphic this: in a base class method, returning this typed as this means subclasses automatically get the correct return type: class Builder { setValue(v: string): this { this.value = v; return this; } } — if you subclass Builder and call setValue(), the return type is the subclass type, not just Builder. Enables fluent builder patterns where methods chain correctly through inheritance. this parameter: TypeScript allows declaring an explicit this parameter as the first parameter of a function to specify the required type of this when the function is called: function greet(this: User, greeting: string): void { console.log(greeting + this.name); }. This is a TypeScript-only parameter — it is erased from the compiled output. It prevents calling the function in contexts where this would have the wrong type.

Intermediate

Strict null checks ("strictNullChecks": true) is one of the most impactful TypeScript settings. Without it (the default in older TypeScript versions), null and undefined are silently assignable to every type — you could assign null to a string variable and TypeScript would not complain. This mirrors JavaScript's behavior but defeats the purpose of type safety. With strict null checks enabled, null and undefined become their own types and are NOT automatically assignable to other types. To allow null: let name: string | null = null — you must explicitly include it in the union. This forces you to handle null cases, which eliminates the notorious "Cannot read properties of null" runtime errors. It also unlocks TypeScript's control flow narrowing for null checks — after if (user !== null), TypeScript knows user is not null. Always enable strict null checks — the slight increase in boilerplate is a worthwhile trade-off for dramatically safer code.

Intermediate

useUnknownInCatchVariables (TypeScript 4.0+, enabled by strict mode) changes the default type of catch clause error variables from any to unknown. Before: try { ... } catch (error) { error.message; /* valid, error was any */ }. With this option: catch (error) { error.message; /* TypeScript error! error is unknown */ }. You must narrow the type first: if (error instanceof Error) { error.message; }. This is much safer because you cannot assume what type was thrown — JavaScript allows throwing any value (throw "a string", throw 42, not just Error objects). Forcing unknown prevents you from blindly accessing error.message when the thrown value might not be an Error instance. Explicitly type the variable as unknown in older TypeScript: catch (error: unknown). Best practice: always narrow catch variables before using them — check error instanceof Error and handle both cases.

Intermediate

Recursive types are types that reference themselves in their own definition, allowing you to model arbitrarily nested data structures. TypeScript supports recursive type aliases. Linked list: type ListNode<T> = { value: T; next: ListNode<T> | null };. Tree node: type TreeNode<T> = { value: T; children: TreeNode<T>[] };. JSON value: type JSONValue = string | number | boolean | null | JSONValue[] | { [key: string]: JSONValue }; — this recursive definition correctly models all valid JSON. Nested partial: type DeepPartial<T> = { [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K] }; — makes all nested properties optional. TypeScript handles recursive types gracefully and they are evaluated lazily, preventing infinite loops in the type checker. They are essential for modeling file system structures, DOM trees, ASTs, and deeply nested configuration objects.

Intermediate

Exclude<T, U> and Extract<T, U> are complementary utility types for filtering union members. Exclude removes from union T all members assignable to U: Exclude<"a" | "b" | "c", "a">"b" | "c". Think of it as a set difference: T minus U. Extract keeps from union T only members assignable to U: Extract<"a" | "b" | "c", "a" | "b">"a" | "b". Think of it as a set intersection: T ∩ U. Both use conditional types and distribute over union members. Use Exclude when: you have a broad union and want to remove specific members (e.g., remove null/undefined — though NonNullable is more idiomatic). Use Extract when: you want to keep only a specific subset of a union (e.g., extract only function types from a union). Remember: both work on unions, not object shapes — use Omit/Pick for object properties.

Intermediate

Higher-order types are types that take other types as arguments and/or return types — analogous to higher-order functions in functional programming. All generic utility types are higher-order: Partial<T>, Record<K, V>, etc. You can compose them: type PartialRecord<K extends keyof any, V> = Partial<Record<K, V>>;. Type-level functions: mapped types and conditional types act as type-level functions. A generic mapped type like type Stringify<T> = { [K in keyof T]: string } is a type-level function that takes T and returns a new type. You can create complex type transformations by composing these: type MakeGetters<T> = { [K in keyof T as `get\${Capitalize<string & K>}`]: () => T[K] };. Higher-order types enable powerful meta-programming in TypeScript — building type-safe APIs, ORMs, validation libraries, and framework integrations that would be impossible with simple type annotations.

Intermediate

Structural typing (also called "duck typing" or "nominal-free typing") means TypeScript determines type compatibility based on the structure (shape) of a type — the properties and methods it has — rather than by explicit declarations or type names. If two types have the same structure, they are compatible, regardless of their names. Example: interface Cat { name: string; meow(): void; } class Dog { name: string = "Rex"; meow() {} } — a Dog instance is assignable to Cat because it has the same structure. This contrasts with nominal typing (used in Java/C#), where only explicitly declared types can be used in place of each other. Structural typing makes TypeScript very flexible and great for working with existing JavaScript patterns. However, it can cause surprises: two completely unrelated types can be compatible if their shapes align. For cases where you want nominal-like behavior (e.g., ensuring an ID from table A cannot be used for table B), use branded types.

Advanced

Since TypeScript uses structural typing, two types with the same structure are interchangeable — which is sometimes undesirable. Branded types (opaque types) use a phantom property trick to make structurally identical types incompatible, simulating nominal typing. Pattern: type UserId = string & { readonly _brand: "UserId" }; type PostId = string & { readonly _brand: "PostId" };. Both are strings at runtime, but TypeScript treats them as different types — you cannot accidentally pass a PostId where a UserId is expected. Create branded values with a cast: function toUserId(id: string): UserId { return id as UserId; }. Common use cases: ID types (user ID, order ID), validated values (SanitizedString, EmailAddress, ValidatedEmail), units (Meters, Kilograms). The phantom property (_brand) never exists at runtime — it is purely a type-system trick. More ergonomic with a helper: type Brand<T, B> = T & { readonly _brand: B }; type UserId = Brand<string, "UserId">;.

Advanced

TypeScript 4.7 introduced explicit variance annotations with in and out modifiers on generic type parameters, giving you direct control over how the type-checker handles subtype relationships. out T marks T as covariant — T only appears in output positions (return types): interface Getter<out T> { get(): T; }. in T marks T as contravariant — T only appears in input positions (parameters): interface Setter<in T> { set(value: T): void; }. in out T marks as invariant — T appears in both positions. Benefits: (1) TypeScript can skip expensive variance inference for complex types, significantly improving compilation performance. (2) You explicitly document the intended variance, catching bugs when you accidentally use T in the wrong position. (3) Better error messages. TypeScript still infers variance automatically, but adding annotations enforces the intent and speeds up type-checking for deeply parameterized generic types in large codebases.

Advanced

The satisfies operator (TypeScript 4.9) addresses the tension between type annotation and type inference. Normally, annotating a variable with a type narrows inference — you lose the specific inferred types of individual properties. Without satisfies: const palette: Record<string, string | number[]> = { red: "#f00", green: [0, 255, 0] }; palette.red.toUpperCase(); // Error! red could be string or number[]. With type assertion: you lose validation. With satisfies: const palette = { red: "#f00", green: [0, 255, 0] } satisfies Record<string, string | number[]>;. Now TypeScript validates the structure matches the type (errors if you add an invalid property), but infers the most specific types for each property: palette.red is string, palette.green is number[]. The satisfies operator is particularly useful for configuration objects, theme tokens, route maps, and any case where you want type validation without losing the specificity of the inferred type.

Advanced

The built-in Partial<T> only makes top-level properties optional. A DeepPartial recursively makes all nested properties optional as well. Implementation using conditional types and mapped types: type DeepPartial<T> = T extends object ? { [P in keyof T]?: DeepPartial<T[P]> } : T;. This checks if T is an object type — if so, it maps over keys making each optional and recursively applying DeepPartial. If T is a primitive, it returns T as-is. For more robustness (handling arrays, functions, dates): type DeepPartial<T> = T extends Function | Date | RegExp ? T : T extends Array<infer U> ? Array<DeepPartial<U>> : T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T;. Use cases: deeply nested update objects (e.g., updating nested config), test factories where you want to override only specific nested properties, state management where partial updates can apply at any depth.

Advanced

Both interfaces and abstract classes define contracts, but they differ fundamentally. Interface: purely structural — describes shape only, no implementation. Cannot have constructors. A class can implement multiple interfaces. Completely erased at runtime. Can be used to describe non-class objects. Supports declaration merging. Best for defining the shape of an API without coupling to any implementation. Abstract class: can have both abstract methods (no implementation) and concrete methods (full implementation). Can have a constructor, fields, and state. A class can only extend ONE abstract class. Exists at runtime as a constructor function. Supports access modifiers on methods. Best when you want to share implementation code and enforce a contract simultaneously — the template method pattern. Choosing: use interface when you only need a contract (shape); use abstract class when you need shared behavior. Many TypeScript experts recommend favoring interfaces + composition over abstract class inheritance. Abstract classes create tighter coupling but less boilerplate for shared logic.

Advanced

Excess property checking is a special TypeScript behavior where it raises an error when you assign an object literal directly to a typed variable if the literal has properties that the type does not declare. Example: interface Point { x: number; y: number; } const p: Point = { x: 1, y: 2, z: 3 }; — TypeScript reports an error because z is not in Point. This seems to contradict structural typing (a type with x, y, z is structurally compatible with Point which only requires x and y). The difference: excess property checking only applies to object literal assignments, not to variable-to-variable assignments: const obj = { x: 1, y: 2, z: 3 }; const p: Point = obj; — no error, because obj is not a fresh literal. This is intentional — when you write a literal directly against a type, you almost certainly made a typo if you included extra properties. Bypass excess checking by using an intermediate variable or a type assertion. Excess property checking also applies to function arguments that are object literals.

Advanced

Key remapping in mapped types (TypeScript 4.1) uses the as clause to transform property names during mapping. Syntax: type Mapped = { [K in keyof T as NewKeyType]: T[K] }. Generate getter methods: type Getters<T> = { [K in keyof T as `get\${Capitalize<string & K>}`]: () => T[K] };. For interface User { name: string; age: number; }, Getters<User> produces { getName(): string; getAge(): number; }. Filter properties during mapping by using never: type NoFunctions<T> = { [K in keyof T as T[K] extends Function ? never : K]: T[K] }; — remapping to never removes that key from the result. Combine with template literals for prefix/suffix transformations: type Prefixed<T, P extends string> = { [K in keyof T as `\${P}\${string & K}`]: T[K] };. Key remapping enables advanced type transformations like generating event handler types, API endpoint types, and form field types from data model types.

Advanced

The infer keyword inside conditional types introduces a type variable that TypeScript fills in by pattern matching on the type structure. It enables extracting parts of a type. Multiple infers: you can use multiple infer in one condition: type Head<T extends any[]> = T extends [infer H, ...any[]] ? H : never; — extracts the first element of a tuple. type Tail<T extends any[]> = T extends [any, ...infer T] ? T : never; — all but the first. Infer in co/contravariant positions: in covariant positions (return type), multiple infers for the same variable produce a union. In contravariant positions (function parameters), they produce an intersection. Infer for deeply nested: type UnpackPromise<T> = T extends Promise<infer U> ? UnpackPromise<U> : T; — recursively unwraps nested promises. TypeScript 4.7 added the ability to use infer with variance annotations: infer T extends string — constrains the inferred type.

Advanced

TypeScript handles circular (self-referential) types through lazy evaluation — types are resolved on demand rather than eagerly. This prevents infinite loops in the type checker. Directly recursive interfaces: always valid — interface Node { children: Node[]; }. Directly recursive type aliases (TS 3.7+): valid when used in object types — type Node = { children: Node[] };. Previously, type aliases had to use interfaces for recursion. Mutually recursive types: two types that reference each other — type Even = { next: Odd } type Odd = { next: Even }; — valid. Limitations: TypeScript may produce simplified error messages for deeply recursive types and may limit recursion depth for complex generic recursive types (you sometimes see "Type instantiation is excessively deep" errors). Workarounds: use interface instead of type alias (interfaces are always lazily evaluated), reduce recursion depth, or use conditional type tricks to provide a base case.

Advanced

These three types are commonly confused. unknown: the safest — represents any possible value including null, undefined, numbers, strings, objects. You cannot access any properties on unknown without type narrowing. {} (empty object type): represents any non-nullish value — accepts strings, numbers, booleans, functions, objects, but NOT null or undefined (in strict mode). Despite the syntax, it does NOT mean "an empty object" — it means "any value that is not null or undefined." TypeScript allows calling any property on {} values — but those accesses are untyped. object: represents any non-primitive value — objects, functions, arrays, but NOT string, number, boolean, symbol, or bigint. Useful when you want to accept object references but not primitive values. Usage guidance: use unknown when the type is truly unknown and you want to force narrowing. Avoid {} — it is surprisingly broad. Use object when you want to accept any object-like value. For typed objects, use specific interfaces or Record<string, unknown>.

Advanced

Const type parameters (TypeScript 5.0) allow you to mark a generic type parameter with const, causing TypeScript to infer the narrowest (most specific) type for that parameter — equivalent to adding as const to the passed argument. Without const: function identity<T>(value: T): T { return value; } const result = identity(["a", "b"]); — T is inferred as string[], result type is string[]. With const: function identity<const T>(value: T): T { return value; } const result = identity(["a", "b"]); — T is inferred as readonly ["a", "b"], the narrowest possible type. This is particularly useful for functions that work with literal values and need to preserve the exact type without requiring callers to add as const everywhere. Common use case: type-safe routing, event systems, or any API where the literal type of arguments matters for the return type's correctness.

Advanced

TypeScript 5.2 introduced the using and await using declarations, implementing the ECMAScript explicit resource management proposal. They automatically call a resource's [Symbol.dispose]() (or [Symbol.asyncDispose]()) method when the variable goes out of scope — similar to using in C# or try-with-resources in Java. Example: { using file = openFile("data.txt"); file.read(); } // file[Symbol.dispose]() is called automatically here. For async resources: await using conn = await getDbConnection(); // conn[Symbol.asyncDispose]() called on exit. No finally block needed. The DisposableStack and AsyncDisposableStack classes help manage multiple resources. This replaces the verbose try-finally pattern for resource cleanup. Useful for: file handles, database connections, network sockets, locks, and any resource that needs deterministic cleanup. TypeScript provides the Disposable and AsyncDisposable interfaces to type-check objects used with using.

Advanced

TypeScript's assignability rules (based on structural compatibility) determine when a value of type A can be used where type B is expected. A type S is assignable to type T if S has at least the same properties with compatible types as T — S can have more properties, but not fewer. For functions: a function is assignable to another if its parameters are compatible (contravariant — more general is OK) and its return type is compatible (covariant — more specific is OK). Key rules: Every type is assignable to itself. never is assignable to every type. Every type is assignable to unknown. Only never is assignable to never. Subclasses are assignable to base classes. Discriminated unions: members are assignable to the union, not vice versa. Optional properties: { name: string } is assignable to { name?: string } but not the reverse. Understanding assignability is foundational to TypeScript — all type errors are ultimately assignability violations. The TypeScript spec details the exact algorithm in terms of "Is type A assignable to type B?"

Advanced

NoInfer<T> (TypeScript 5.4) is a built-in utility type that prevents TypeScript from using a generic type parameter as an inference site. This gives you control over which call-site argument drives type inference. Problem it solves: function createStore<T>(initial: T, fallback: T): T { ... }. If you call createStore("hello", 0), TypeScript infers T as string | number — not the desired behavior. With NoInfer: function createStore<T>(initial: T, fallback: NoInfer<T>): T { ... } — T is inferred only from initial, and fallback must be compatible with the already-inferred T. Calling createStore("hello", 0) now gives a type error because 0 is not assignable to string. NoInfer is implemented as an intrinsic type (not expressible in user-land TypeScript). It is essential for APIs where one argument should constrain others without participating in inference itself.

Advanced

Building a type-safe event emitter in TypeScript ensures that event names and their payload types are connected and validated at compile time. Using a generic event map: type EventMap = { click: MouseEvent; keydown: KeyboardEvent; data: { message: string } };. A typed emitter class: class TypedEmitter<Events extends Record<string, unknown>> { on<K extends keyof Events>(event: K, listener: (data: Events[K]) => void): void { ... } emit<K extends keyof Events>(event: K, data: Events[K]): void { ... } }. Usage: const emitter = new TypedEmitter<EventMap>(); emitter.on("click", (e) => e.clientX); // MouseEvent is known emitter.emit("data", { message: "hello" }); // payload type enforced. Passing a wrong payload type or misspelling an event name produces a compile-time error. Libraries like typed-emitter, eventemitter3 with types, and mitt provide ready-made type-safe event emitters. This pattern is fundamental to building type-safe plugin systems, state management, and component communication.

Advanced

TypeScript 4.9 introduced the accessor keyword for class fields, which automatically generates getter and setter methods using the ECMAScript accessor property proposal. Syntax: class Person { accessor name: string = ""; }. This is shorthand for: class Person { private _name: string = ""; get name() { return this._name; } set name(value: string) { this._name = value; } }. The generated getter and setter use a private backing field (implementation details vary). Benefits: cleaner syntax for reactive properties, better integration with decorators (the @accessor decorator can intercept get/set operations), and consistent behavior across class hierarchies. The accessor keyword is particularly useful with the new decorator standard (stage 3) where @accessor decorators can add validation, change notification, or transformation logic to individual properties. It enables patterns like observable properties in UI frameworks without verbose boilerplate.

Advanced

A type-safe builder pattern uses TypeScript's type system to track what properties have been set and enforce that required properties are provided before the object is built. Using phantom types and conditional types: type HasName = { name: true }; type HasAge = { age: true }; class UserBuilder<T = {}> { private data: Partial<User> = {}; setName(name: string): UserBuilder<T & HasName> { this.data.name = name; return this as any; } setAge(age: number): UserBuilder<T & HasAge> { this.data.age = age; return this as any; } build(this: UserBuilder<HasName & HasAge>): User { return this.data as User; } }. With this, calling build() before setName() and setAge() is a compile-time error: new UserBuilder().setName("Alice").build(); — error, missing age. new UserBuilder().setName("Alice").setAge(30).build(); — valid. Each setter returns a more specific type that tracks progress, and build() requires the intersection of all required phantom types.

Advanced

TypeScript type-checking is computationally intensive and can slow down build times in large codebases. Common performance bottlenecks: (1) Complex generic types and deeply recursive conditional types that cause excessive type instantiation. (2) Large union types (thousands of members). (3) Too many files processed together. (4) Re-exporting types through many re-export layers. Optimization strategies: (1) Use interface instead of type for object shapes — interfaces are cached and faster. (2) Enable project references (--build) to compile sub-projects independently with incremental builds. (3) Use "incremental": true in tsconfig for incremental type-checking that only rechecks changed files. (4) Use "skipLibCheck": true to skip checking declaration files. (5) Use isolatedModules with a fast transpiler (Babel/SWC/esbuild) for development, with separate tsc for type-checking. (6) Avoid recursive types that are too deep. (7) Break up large union types into intermediate types. Use the TypeScript compiler's --extendedDiagnostics and --generateTrace flags to identify bottlenecks.

Advanced

Project references (TypeScript 3.0) allow you to split a large TypeScript codebase into smaller, independently compilable projects. Each sub-project has its own tsconfig.json with "composite": true and declares its dependencies on other sub-projects via "references". Building with tsc --build (or tsc -b) compiles each sub-project in dependency order and only recompiles what changed. Benefits: (1) Faster builds — only changed projects and their dependents recompile, not the entire codebase. (2) Logical separation — clear package boundaries and dependency graph. (3) Independent versioning — each project can have its own tsconfig settings. (4) Better IDE performance — editors can load only relevant parts of a large codebase. Configure: the root tsconfig references sub-projects, each sub-project has "composite": true and "declarationDir" set. Project references are essential for monorepos and large enterprise TypeScript applications.

Advanced

TypeScript exists at two levels simultaneously. Value-level programming is regular JavaScript/TypeScript code that runs at runtime — variables, functions, classes, objects, conditional statements, loops. Type-level programming is computation that happens only at compile time in the type checker — generic types, conditional types, mapped types, template literal types, infer. The type level is a completely separate, Turing-complete language built into TypeScript's type system. At the type level: conditional types are if-else, mapped types are loops, recursive generics are recursion. Interesting property: any type computation that can be expressed in the type system is guaranteed to terminate (TypeScript has recursion depth limits). TypeScript engineers have implemented type-level: JSON parsers, arithmetic operations, string parsers, Fibonacci sequences, and more — all in the type system. Practical examples of type-level computation: Awaited<T> recursively unwraps promises, DeepPartial<T> recursively makes nested properties optional, template literal types compute string transformations. Understanding both levels is key to mastering advanced TypeScript.

Advanced

Abstract constructor types describe the type of an abstract class — a class that cannot be directly instantiated with new. The type for a regular class (constructable) is new (...args: any[]) => InstanceType. The type for an abstract class is abstract new (...args: any[]) => InstanceType. This distinction matters when writing mixins or higher-order class functions that need to accept abstract classes: type AbstractConstructor<T> = abstract new (...args: any[]) => T;. Mixin that works with abstract classes: function Serializable<T extends AbstractConstructor<object>>(Base: T) { abstract class Serializable extends Base { abstract serialize(): string; } return Serializable; }. Without the abstract keyword on the constructor type, passing an abstract class to a function expecting a constructable class causes a type error — because abstract classes cannot be new-d. TypeScript 4.2 added support for abstract construct signatures to properly type abstract class references.

Advanced
Back to All Topics 97 questions total