Generics
IntermediateGenerics carry a type through a function instead of losing it. Familiar from Java, with two differences worth knowing: inference is much stronger, and there is no erasure workaround because there is nothing to work around.
Overview
A generic is a type parameter — a placeholder filled in at each call site so the relationship between input and output survives. Java engineers already have this instinct, and most of it transfers directly. What differs is that TypeScript infers type arguments aggressively, so you rarely write them explicitly, and that constraints use `extends` in a structural rather than nominal sense. The habit worth building is reaching for a generic only when a type genuinely flows through; a generic used once in a signature is usually a disguised `any`.
Carrying a Type Through
Without a generic the type is lost. With one, it survives.
// Loses the type
function firstAny(arr: any[]): any { return arr[0] }
const n = firstAny([1, 2, 3]) // any — no help downstream
// Keeps it
function first<T>(arr: T[]): T | undefined { return arr[0] }
const m = first([1, 2, 3]) // number | undefined — inferred, no <number> needed
// Two parameters, related
function mapValues<T, U>(obj: Record<string, T>, fn: (v: T) => U): Record<string, U> {
return Object.fromEntries(
Object.entries(obj).map(([k, v]) => [k, fn(v)]),
) as Record<string, U>
}
// A typed fetch wrapper — the pattern you will actually write
async function api<T>(path: string): Promise<T> {
const res = await fetch(path)
if (!res.ok) throw new Error(res.statusText)
return res.json() as Promise<T>
}
const problems = await api<Problem[]>('/api/problems')Constraints
extends narrows what T can be, which is what lets you use its members.
// Unconstrained T has no members you can touch
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b
}
longest('arrays', 'graphs') // string
longest([1, 2], [1, 2, 3]) // number[]
longest(1, 2) // Error — number has no length
// keyof, for type-safe property access
function pluck<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]
}
pluck(user, 'name') // string
pluck(user, 'emial') // Error — not a key of T
// Defaults
interface Paged<T = unknown> { items: T[]; total: number }
// Where Java says <T extends Comparable<T>>, TypeScript says
// <T extends { compareTo(other: T): number }> — structural, not nominal.When Not to Use One
A type parameter that appears once is not doing anything.
// Pointless — T appears once, so it is just 'any' with extra steps
function log<T>(value: T): void { console.log(value) }
function log2(value: unknown): void { console.log(value) } // same thing, clearer
// Also pointless — the constraint is the whole type
function len<T extends string>(s: T): number { return s.length }
function len2(s: string): number { return s.length } // identical
// Useful — T connects two positions
function keyBy<T, K extends keyof T>(items: T[], key: K): Map<T[K], T>
// Rule of thumb: a type parameter must appear at least twice —
// once to receive the type, once to use it.Key Points to Remember
- 1A generic preserves the relationship between input and output types instead of collapsing to any
- 2TypeScript infers type arguments from the call site, so explicit <T> is rarely needed
- 3extends constrains a type parameter, which is what allows you to access members on it
- 4K extends keyof T is the pattern for type-safe property access by name
- 5A type parameter appearing only once in a signature is doing nothing — use unknown or the concrete type
Interview Questions
Sign in to ask AriaWhy use a generic instead of any for a function that returns an array element?
What does K extends keyof T let you do, and why is it safer than a string parameter?
When is a generic type parameter unnecessary?
Ask Aria about Generics
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.