Generics
Without generics, writing a reusable function forces an unpleasant choice: either lock it to one specific type (not reusable), or use any (reusable, but loses all type safety).
🎯 What you'll learn
- The Problem Generics Solve
- Generic Functions — Type Parameters
- Multiple Type Parameters
- Generic Interfaces and Type Aliases
- Generic Classes
1The Problem Generics Solve
Without generics, writing a reusable function forces an unpleasant choice: either lock it to one specific type (not reusable), or use any (reusable, but loses all type safety).
function firstAny(arr: any[]): any {
return arr[0];
}
const num = firstAny([1, 2, 3]); // typed `any` — no safety, could be anything
const str = firstAny(["a", "b"]); // also `any` — TypeScript forgot it was a string array2Generic Functions — Type Parameters
A type parameter (conventionally T) lets a function stay fully reusable while preserving the exact type relationship between its inputs and output.
function first<T>(arr: T[]): T {
return arr[0];
}
const num = first([1, 2, 3]); // T inferred as number → num: number
const str = first(["a", "b"]); // T inferred as string → str: stringT is a placeholder decided per call site — first([1,2,3]) and first(["a","b"]) each independently substitute their own concrete type for T, without you writing it explicitly (thanks to inference, previewed in the Type Inference topic and covered in depth in Part 2 of this topic).
3Multiple Type Parameters
function pair<A, B>(first: A, second: B): [A, B] {
return [first, second];
}
const p = pair("age", 30); // [string, number], both inferred independently4Generic Interfaces and Type Aliases
interface Box<T> {
value: T;
}
const numberBox: Box<number> = { value: 42 };
type Pair<A, B> = { first: A; second: B };
const p: Pair<string, number> = { first: "age", second: 30 };5Generic Classes
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
}
const numberStack = new Stack<number>();
numberStack.push(1);
// numberStack.push("a"); // ❌ error — this Stack instance is locked to T = numberOnce instantiated with a concrete type (Stack<number>), every method on that instance is checked against that specific substitution — push only accepts number for that particular stack.
6Explicit Type Arguments
function wrapInArray<T>(value: T): T[] {
return [value];
}
const explicit = wrapInArray<string>("hello"); // explicitly specify T, instead of relying on inference
const inferred = wrapInArray("hello"); // same result — T inferred as string automaticallyExplicit type arguments are needed when there's nothing for the checker to infer from (an empty array literal with no other context, for example) or when you deliberately want a wider type than inference alone would choose.
7Default Type Parameters
interface ApiResponse<T = unknown> {
data: T;
status: number;
}
const response: ApiResponse = { data: "anything", status: 200 }; // T defaults to unknown
const typedResponse: ApiResponse<string> = { data: "hello", status: 200 }; // T explicitly stringA default type parameter behaves like a default function parameter — used only when no type argument is supplied and nothing can be inferred.
1How Generic Type Inference Actually Works
When you call first([1, 2, 3]), TypeScript doesn't guess T — it solves for it by comparing the shape of the argument against the shape of the parameter type, position by position, and reading off whatever concrete type lines up with T.
function first<T>(arr: T[]): T { return arr[0]; }
first([1, 2, 3]);
// parameter shape: T[]
// argument shape: number[]
// aligning them: T = numberThis process is called unification — finding a substitution for the type variable(s) that makes the parameter type and argument type match. It's the same general idea as inference algorithms in other statically-typed languages with generics (Java, C#, Rust), adapted to TypeScript's structural type system.
2Inference from Multiple Arguments — Combining Evidence
function combine<T>(a: T, b: T): T[] {
return [a, b];
}
combine(1, 2); // T = number
combine(1, "two"); // T = number | string — the checker computes the BEST COMMON TYPE across bothWhen multiple arguments all constrain the same type parameter, TypeScript doesn't just use the first one — it collects evidence from every position and computes the best common type across all of them (same mechanism as the Type Inference topic's array best-common-type rule, applied here to a type parameter instead of an array's element type).
3When Inference Fails: Needing Explicit Type Arguments
function createEmpty<T>(): T[] {
return [];
}
const arr = createEmpty(); // T has NOTHING to infer from — no argument mentions T at all
// arr's type ends up as `unknown[]` in strict settings, not a useful specific type
const arr2 = createEmpty<string>(); // must supply T explicitly hereInference can only work when T appears somewhere in the function's actual parameters — a type parameter that only appears in the return type has no argument to unify against, forcing an explicit type argument.
4Generic Constraints (Preview)
function getLength<T extends { length: number }>(item: T): number {
return item.length;
}extends here restricts which types are valid for T — full depth is the Generic Constraints topic; the short version is that unconstrained T can be anything, which means the function body can only do things valid for literally every possible type (nothing, essentially) until a constraint narrows what's guaranteed to exist.
5Generics Are Erased Too — No Runtime Type Parameters
function identity<T>(value: T): T {
return value;
}compiles to:
function identity(value) {
return value;
}T never exists at runtime in any form — there's no way to inspect "what was T instantiated as" inside the function body at runtime, unlike languages with runtime generic reflection (C#'s typeof(T)). If you need runtime knowledge of the type, it must be passed as an ordinary runtime value (e.g. a constructor function or a string tag), not derived from the erased type parameter.
Under the HoodInstantiation, Not Monomorphization▾
TypeScript checks each instantiation, but does NOT generate separate compiled code per type
Unlike C++ templates (which generate a separate compiled function per concrete type used — "monomorphization"), TypeScript generics are checked once, generically, against the constraint (or unknown if unconstrained), and then erased to a single, ordinary JavaScript function. There is exactly one identity function in the compiled output, regardless of how many different types call it — the type-checking happens per call site, but the code is never duplicated.
TypeScript: identity<T>(value: T): T → ONE generic definition, checked structurally
Compiled JS: function identity(value) { return value; } → ONE function, used for every TThe unification algorithm walks structurally, not just top-level
For more deeply nested generic positions, unification recurses through the structure:
function unwrap<T>(box: { value: T }): T { return box.value; }
unwrap({ value: 42 });
// parameter shape: { value: T }
// argument shape: { value: 42 } → { value: number }
// walking into the matching property: T = numberThe checker doesn't just compare top-level types — it walks into corresponding structural positions (object properties, array elements, function parameter/return positions) recursively, collecting a substitution for every type parameter it encounters along the way, however deeply nested.
Type parameters used only in contravariant (input) positions infer differently than covariant (output) positions
When a type parameter appears in multiple argument positions with genuinely different types, and those positions are function parameters (contravariant), the checker tends toward the most specific common type; when it appears in return/output (covariant) positions across multiple candidates, it tends toward a union (section 2's example). This asymmetry reflects real soundness concerns — inferring too broad a type for something you're about to call as a function is more likely to cause problems than inferring a broad type for something you're only ever going to read.
⚠️ Common Pitfalls
- Expecting a type parameter to give runtime type information —
Tis fully erased; nothing about it exists once compiled, per section 5. - Writing an unconstrained
Tand then trying to useT-specific operations — with no constraint,Tcould be anything, so the function body can't assume it has any properties or methods at all (see the Generic Constraints topic). - Assuming generics generate separate compiled code per type, the way C++ templates do — TypeScript compiles to exactly one function; all the "specialization" is purely a compile-time checking activity.
- Forgetting inference needs
Tin the parameters — a type parameter that only appears in the return type can't be inferred from arguments; it needs either an explicit type argument or a contextual type at the call site.
📋 Quick Reference
function f<T>(x: T): T {} → T inferred by unifying parameter shape against argument shape
f<string>(x) → explicit type argument, when inference has nothing to work from
generics erased entirely at compile → no runtime reflection of T, ever
one compiled function, not one per T → checking is per-call-site, code is not duplicated🎤 Interview Q&A
Q1Why use generics?core▾
To write reusable code that preserves type relationships: function first<T>(arr: T[]): T | undefined returns the element type of whatever array is passed, instead of any.
- Generic interfaces?
interface ApiResponse<T> { data: T; error?: string }.
Q2How do you constrain a generic?core▾
With extends: function getProp<T, K extends keyof T>(obj: T, key: K): T[K] ensures the key exists and returns the right property type.
- Default type parameters?
<T = string>.
Q3When should you not use generics?senior▾
When a type parameter appears only once (it adds nothing; use the concrete or unknown type), or when it makes signatures unreadable without benefit.
- Rule of thumb? Type parameters should relate two or more things.
Q4How do generics work with classes?core▾
class Repository<T extends { id: string }> { findById(id: string): T | undefined }: each instance is typed for its entity; static members can't use the class's type parameter.
- Generic React components?
function List<T>(props: { items: T[]; render: (t: T) => ReactNode }).
Q5What is variance and why does it matter?core▾
How subtyping of type arguments relates to subtyping of generic types: arrays are covariant (unsound for writes), function parameters are contravariant under strictFunctionTypes. TS 4.7+ supports in/out annotations.
- Example of unsoundness? Assigning
Dog[]toAnimal[]then pushing a Cat.
🛠️ Practice exercises
Each exercise is a file in this topic folder. Code exercises are self-checking: run them and make the checks pass.