TypeScript Generics
TL;DR
Generics are type-level parameters. They let a function, class, or type be reused across many concrete types without losing type information (the alternative is any, which loses everything). The senior moves: constraining generics with extends, using keyof + lookup types to make APIs type-safe against an object’s actual shape, and knowing when not to add a generic.
Interview Q&A
Q: What’s a generic, and why use one over any?
A generic introduces a type variable that’s bound at the call site. The type relationship between input and output is preserved; with any it’s erased.
function identity<T>(value: T): T { return value }
const n = identity(42) // n: number
const s = identity('hello') // s: string
// vs any:
function identityAny(value: any): any { return value }
const x = identityAny(42) // x: any — error-prone
The contract “what comes out has the same type as what went in” is encoded.
Q: Generic function vs generic interface vs generic class — when each?
// Generic function: each call binds T fresh.
function first<T>(arr: T[]): T | undefined { return arr[0] }
// Generic interface: T is bound when the interface is used.
interface Box<T> { value: T }
const a: Box<number> = { value: 1 }
// Generic class: T is bound at instantiation.
class Stack<T> {
private items: T[] = []
push(item: T) { this.items.push(item) }
pop(): T | undefined { return this.items.pop() }
}
const s = new Stack<string>()
Rule of thumb: use a generic function when the type flows through one call; a generic interface/type to describe a parameterized shape; a generic class when state with that shape is held over time.
Q: How do generic constraints (extends) work?
T extends X means “T must be assignable to X.” You get to use X’s members on T inside the function.
function getLength<T extends { length: number }>(x: T): number {
return x.length // allowed — T provably has .length
}
getLength('hello') // ok
getLength([1, 2, 3]) // ok
getLength(42) // error — number has no .length
Constraints narrow what T can be and unlock its members. Without the constraint, x.length would error.
Q: What are default type parameters good for?
A default lets a generic be omitted at the call site and still be useful — common for “container” types and React component generics.
interface ApiResponse<T = unknown> {
data: T
status: number
}
const r1: ApiResponse = { data: 'something', status: 200 } // T = unknown
const r2: ApiResponse<User> = { data: { id: 1 }, status: 200 } // T = User
Defaults also avoid breaking callers when you add a generic later.
Q: What does keyof do, and how do you use it with lookup types?
keyof T is the union of the property names of T. T[K] is the type at key K. Combined, they give you a type-safe “get any property” API:
function pluck<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]
}
const user = { id: 1, name: 'Ada', email: 'a@b.com' }
const id = pluck(user, 'id') // id: number
const name = pluck(user, 'name') // name: string
pluck(user, 'nope') // error — 'nope' not in keyof typeof user
This is the single most common generic pattern in real code (form libraries, ORMs, validators).
Q: How do you type a function that maps an object’s values?
function mapValues<T extends object, U>(
obj: T,
fn: <K extends keyof T>(value: T[K], key: K) => U,
): Record<keyof T, U> {
const out = {} as Record<keyof T, U>
for (const k in obj) out[k] = fn(obj[k] as T[Extract<keyof T, string>], k)
return out
}
The generic K inside fn lets the callback know exactly which key’s value it received.
Q: How do generic constraints interact with inference?
TS infers from arguments. If you constrain too loosely, you lose information; too tightly, you over-restrict callers.
// Too loose: T inferred as { id: number } only — loses the rest of the object.
function withId<T extends { id: number }>(x: { id: number }): T { return x as T }
// Right: T preserved fully through the function.
function withId2<T extends { id: number }>(x: T): T { return x }
const u = withId2({ id: 1, name: 'Ada' }) // u: { id: number; name: string }
The shape “argument is T, return is T” is what preserves narrow inference.
Q: When shouldn’t you reach for generics?
When the generic is never actually used as a type relationship. If T appears only once in the signature, it’s almost certainly wrong — you wanted a constraint, not a generic.
// Smell — T appears only in return position. Caller must specify it.
function parse<T>(json: string): T { return JSON.parse(json) }
const u = parse<User>('{}') // looks safe; isn't. T is unverified.
// Better:
function parse(json: string): unknown { return JSON.parse(json) }
// caller validates with zod/io-ts/etc and gets a typed value
A generic that appears only on the return is just a polite any.
Gotchas / edge cases
- Arrow generics in
.tsx—<T>(x: T) => xlooks like JSX. Use<T,>(x: T) => x(trailing comma) or<T extends unknown>(x: T) => x. - Over-constraining —
T extends stringwhen you really meant “any value”; the constraint leaks into the public API. Objectvs{}vsobjectvsRecord<string, unknown>— all different. PreferRecord<string, unknown>or a specific shape.- Inference can collapse to a base type — passing two different object types into a function generic on
Tmay inferTas their union, not what you wanted; use a constraint to anchor it. - Generic defaults don’t fix everything — a default of
unknownstill requires narrowing before use. - Higher-kinded types don’t exist in TS — you can’t write
F<T>where F is itself a generic parameter (noFunctor<F>). You work around with helper types.
What a senior is expected to say
A junior says “generics make code reusable.” A senior says: generics encode relationships between types (input shape ↔ output shape, key ↔ value type, request ↔ response shape). Constraints (extends) plus keyof T + lookup types T[K] are the workhorse pattern in real codebases — form libraries, query builders, ORMs are all built on it. The senior also flags the anti-pattern: a generic that only appears in the return position is a polite any masquerading as type safety.
Cross-references
- Conditional types and
inferbuild on this: 02_conditional_and_mapped_types.md - Utility types reimplemented with these primitives: 04_utility_types_from_scratch.md
- React component generics: 08_react_typing.md