Generics are the reason a function like Array.prototype.map can transform string[] into number[] and have TypeScript track the correct type the whole way through — without a generic, that's not possible to express accurately.
The problem generics solve
Without generics, a reusable function has to either give up on specific types or write one version per type:
// Loses type information — box.contents is always `any`
function makeBox(value: any) {
return { contents: value };
}
// Repeats the same logic for every type
function makeStringBox(value: string) { return { contents: value }; }
function makeNumberBox(value: number) { return { contents: value }; }Neither is good — the first loses type safety, the second doesn't scale.
The generic version
function makeBox<T>(value: T) {
return { contents: value };
}
const stringBox = makeBox("hello"); // { contents: string }
const numberBox = makeBox(42); // { contents: number }<T> declares a type parameter — a placeholder type that gets filled in based on whatever's actually passed. TypeScript infers T from the argument automatically; you rarely need to write makeBox<string>("hello") explicitly.
Generic interfaces and types
The same pattern applies to interfaces, not just functions — useful for anything that wraps another type generically:
interface ApiResponse<T> {
data: T;
status: number;
error: string | null;
}
async function fetchUser(): Promise<ApiResponse<User>> {
const res = await fetch("/api/user");
return res.json();
}ApiResponse<User> and ApiResponse<Product> share the exact same shape (data, status, error) but data is correctly typed as User or Product respectively — one interface definition instead of one per response type.
Constraining a generic with extends
Sometimes a generic function needs to guarantee the type has at least certain properties, without locking it to one exact type:
interface HasId {
id: string;
}
function findById<T extends HasId>(items: T[], id: string): T | undefined {
return items.find((item) => item.id === id);
}
findById([{ id: "1", name: "Ada" }], "1"); // works — has an id
findById([{ name: "Ada" }], "1"); // Error: missing "id"T extends HasId means "T can be any type, as long as it has at least an id: string property" — the function works with any object shape that satisfies that minimum, while still preserving the full original type in the result.
Multiple type parameters
Generics aren't limited to one type parameter — a function combining two different collections often needs two:
function zip<A, B>(a: A[], b: B[]): [A, B][] {
return a.map((item, i) => [item, b[i]]);
}
zip([1, 2, 3], ["a", "b", "c"]);
// [[1, "a"], [2, "b"], [3, "c"]] — typed as [number, string][]Default type parameters
A generic can specify a default, used when nothing more specific is provided at the call site:
interface FetchOptions<T = unknown> {
url: string;
parser?: (raw: string) => T;
}
const opts: FetchOptions = { url: "/api/data" }; // T defaults to unknownThis keeps the common, unspecified case simple while still allowing callers to specify FetchOptions<User> explicitly when they know the actual shape.
Generic classes
The same pattern applies to classes, useful for a data structure that should work with any type consistently:
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(2);
const stringStack = new Stack<string>();
stringStack.push("a"); // stringStack.push(1) would be a type errorOne Stack implementation serves both number and string (or any other type) stacks, each fully type-checked as its own specific type — without generics, this would require either a separate class per type or falling back to any and losing type safety entirely.
keyof and generic property access
A common, genuinely useful generic pattern: a function that safely accesses a property by name, with the return type automatically inferred from which property was requested:
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "Ada", age: 30 };
getProperty(user, "name"); // typed as string
getProperty(user, "age"); // typed as number
getProperty(user, "email"); // Error: "email" is not a key of the user objectK extends keyof T constrains K to only the actual property names that exist on T — attempting to access a property that doesn't exist is caught at compile time, not discovered at runtime as undefined.
Generics in React components
The same generic pattern extends to React component props, useful for a component genuinely reusable across different data shapes:
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
}
function List<T>({ items, renderItem }: ListProps<T>) {
return <ul>{items.map((item, i) => <li key={i}>{renderItem(item)}</li>)}</ul>;
}
<List items={users} renderItem={(user) => user.name} />
<List items={products} renderItem={(product) => product.title} />One List component serves both User[] and Product[] data, each call fully type-checked — renderItem is correctly typed as receiving a User in the first case and a Product in the second, inferred automatically from the items array passed.
Common mistakes
- Reaching for a generic when the function only ever needs to work with one concrete type. Generics add real complexity — if a function genuinely only ever handles
string, a plainstringparameter is more readable than an unnecessaryT. - Forgetting
extendswhen a generic function needs to access a specific property. Without a constraint, TypeScript only knowsTis "some type" and won't let you access.idor any other property on it, even if every real caller happens to pass an object that has one. - Using
anyinstead of a generic to "make an error go away." This is almost always the wrong fix — it silently disables type checking for that value everywhere it flows, rather than preserving type safety through the function the way a generic does. - Over-constraining a generic with a type so specific it might as well not be generic at all — if
T extends ExactlyOneSpecificShape, consider whether the function actually needs a fixed, non-generic parameter type instead.
Related reading
- TypeScript Utility Types Cheat Sheet — shares tags: typescript, javascript, programming (same category).
- How to Migrate a JavaScript Codebase to TypeScript Incrementally — shares tags: typescript, javascript, programming.
- JavaScript Closures Explained: What They Actually Are and Why They Matter — shares tags: javascript, programming.
- The JavaScript Event Loop, Explained With Diagrams — shares tags: javascript, programming.
- Modern Array Methods You Should Be Using Instead of Loops — shares tags: javascript, programming.