What Are Generics?
Generics let you write reusable functions, classes, and interfaces that work with any type while still preserving full type safety. Think of them as type-level parameters — just like function parameters accept values, generics accept types.
// Without generics — loses type information
function first(arr: unknown[]): unknown {
return arr[0];
}
// With generics — type flows through
function first<T>(arr: T[]): T {
return arr[0];
}
const num = first([1, 2, 3]); // number
const str = first(["a", "b"]); // stringConstraints
The extends keyword narrows what types a generic parameter can accept. This lets you call methods or access properties on the parameter without losing generality.
// T must have a 'length' property
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
longest("hello", "hi"); // "hello"
longest([1, 2, 3], [4, 5]); // [1, 2, 3]
// longest(1, 2); // Error: number has no 'length'
// Key constraint — K must be a key of T
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}Common Patterns
These four generic containers appear everywhere in TypeScript codebases. Understanding their type signatures makes reading library types much easier.
Built-in Utility Types
TypeScript ships a library of generic utility types that transform existing types. These cover the vast majority of type manipulation needs without any extra dependencies.
| Utility Type | What It Does |
|---|---|
| Partial<T> | Makes all properties of T optional |
| Required<T> | Makes all properties of T required |
| Readonly<T> | Prevents reassignment of all properties of T |
| Pick<T, K> | Creates a type with only the keys K from T |
| Omit<T, K> | Creates a type without the keys K from T |
| ReturnType<F> | Extracts the return type of a function type F |
| Awaited<T> | Recursively unwraps the resolved value of a Promise<T> |
| NonNullable<T> | Removes null and undefined from T |
Real-World Examples
The most practical use of generics is building typed wrappers around data-fetching and UI components. The pattern below keeps types flowing from the API call all the way to the rendered output with zero casts.
// Generic API response wrapper
interface ApiResponse<T> {
data: T;
error: string | null;
status: number;
}
async function fetchJson<T>(url: string): Promise<ApiResponse<T>> {
const res = await fetch(url);
const data = await res.json() as T;
return { data, error: null, status: res.status };
}
// Usage — T is inferred from the return annotation
const { data } = await fetchJson<User[]>("/api/users");
// ^? User[]
// Generic React component
function List<T extends { id: string | number }>({
items,
render,
}: {
items: T[];
render: (item: T) => React.ReactNode;
}) {
return <ul>{items.map((item) => <li key={item.id}>{render(item)}</li>)}</ul>;
}