A Practical Guide to TypeScript Generics
Generics don't have to be intimidating. Here's a hands-on walkthrough of the patterns you'll actually use day-to-day.
Generics are the feature that separates “I know TypeScript” from “I know TypeScript.” They’re also the feature most likely to produce write-only code if you’re not careful. This guide focuses on the patterns that are genuinely useful — not the type gymnastics that belong in a puzzle book.
The core idea
A generic is a type that takes a parameter. Instead of writing separate functions for different types:
function firstString(arr: string[]): string | undefined {
return arr[0];
}
function firstNumber(arr: number[]): number | undefined {
return arr[0];
}
You write one function that works with any type:
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
const name = first(['Alice', 'Bob']); // string
const age = first([30, 25]); // number
TypeScript infers T from the argument, so you rarely need to specify it explicitly. That’s the whole trick: you’re telling the compiler “I don’t know the exact type yet, but whatever it is, use it consistently.”
Constraining with extends
Raw generics accept anything. Often you need to be more specific:
function getLength<T extends { length: number }>(item: T): number {
return item.length;
}
getLength('hello'); // ✓ string has .length
getLength([1, 2, 3]); // ✓ array has .length
getLength(42); // ✗ number doesn't have .length
The extends clause is a contract: “T can be anything, as long as it has a length property that’s a number.”
The keyof pattern
This is probably the most useful generic pattern in everyday code:
function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
const result = {} as Pick<T, K>;
keys.forEach((key) => {
result[key] = obj[key];
});
return result;
}
const user = { name: 'Alice', age: 30, email: 'alice@example.com' };
const nameAndEmail = pick(user, ['name', 'email']);
// Type: { name: string; email: string }
The compiler knows exactly which keys are valid and what the return type looks like. Try passing 'foo' as a key and you’ll get a compile error.
Generic React components
If you work with React, you’ll write generic components sooner or later. A common pattern is a type-safe select:
interface SelectProps<T> {
options: T[];
value: T;
onChange: (value: T) => void;
getLabel: (item: T) => string;
}
function Select<T>({ options, value, onChange, getLabel }: SelectProps<T>) {
return (
<select
value={options.indexOf(value)}
onChange={(e) => onChange(options[Number(e.target.value)])}
>
{options.map((option, i) => (
<option key={i} value={i}>{getLabel(option)}</option>
))}
</select>
);
}
Now the component works with strings, objects, enums — whatever you pass in. The onChange callback receives the correct type automatically.
When to stop
Generics are powerful, but they have a complexity cost. If you find yourself nesting three levels of generics with conditional types and mapped types, step back and ask: “Would a simpler type with a union work here?”
The goal is code that’s easy to use correctly and hard to use incorrectly. If your generic signature is harder to read than the implementation, you’ve gone too far.
Keep it simple. Let the compiler do the work. That’s what it’s there for.