TypeScript Generics: How to Write Reusable Typed Code
Practice4 minInnovialabs editorial team
In short
A generic is a type parameter that lets one function or type work with different data while keeping precise types, without falling back to any.
Why generics exist
A generic is a parameter for a type. Just as a function takes values, a generic takes types. You write the code once and reuse it with different data, keeping accurate hints and checks.
Without generics you have two bad options: duplicate the function for every type, or use any and lose type safety.
function first(items: any[]): any {
return items[0];
}
const x = first([1, 2, 3]); // any — TypeScript stops checking
With a generic, the type is preserved:
function first<T>(items: T[]): T | undefined {
return items[0];
}
const n = first([1, 2, 3]); // number | undefined
const s = first(['a', 'b']); // string | undefined
T is inferred from the argument, so you usually do not have to write it.
Generic types and interfaces
The most common real example is an API response wrapper:
interface ApiResponse<T> {
data: T;
error: string | null;
}
interface User {
id: string;
name: string;
}
async function getJson<T>(url: string): Promise<ApiResponse<T>> {
const res = await fetch(url);
if (!res.ok) return { data: null as T, error: `HTTP ${res.status}` };
return { data: (await res.json()) as T, error: null };
}
const result = await getJson<User[]>('/api/users');
result.data[0].name; // string, autocomplete works
Here T is passed explicitly, because TypeScript cannot know what the server returns. Keep in mind this is a promise to the compiler, not a check. For external data in critical paths, add runtime validation (for example, with a schema).
Constraints: extends
Sometimes you need the type to have a certain field. That is what a constraint with extends is for:
function byId<T extends { id: string }>(items: T[], id: string): T | undefined {
return items.find((item) => item.id === id);
}
The function accepts an array of any objects that have id: string and returns the same type, not a trimmed-down one.
A constraint with keyof lets you read a property safely:
function pick<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: '1', name: 'Anna', age: 30 };
pick(user, 'name'); // string
pick(user, 'email'); // compile error
Default types
Like function parameters, generics can have defaults:
interface Paginated<T = unknown> {
items: T[];
page: number;
total: number;
}
const a: Paginated = { items: [], page: 1, total: 0 }; // T = unknown
const b: Paginated<User> = { items: [], page: 1, total: 0 }; // T = User
Built-in utility types
TypeScript ships with ready-made generic types that cover most everyday needs:
| Utility | What it does |
|---|---|
Partial<T> | makes every field optional — handy for update forms |
Required<T> | makes every field required |
Pick<T, K> | keeps only the chosen fields |
Omit<T, K> | removes the chosen fields |
Record<K, V> | object with keys K and values V |
ReturnType<F> | a function's return type |
Awaited<T> | the type you get after await |
type UserUpdate = Partial<Omit<User, 'id'>>;
// { name?: string }
The full list is in the official documentation.
Common mistakes
- Generics for their own sake. If
Tappears only once in a signature, you probably do not need it — a plain type will do. - Too many parameters.
<T, U, V, W>is hard to read. If you need them, use meaningful names likeTDataandTError. - Casting instead of checking.
as Thides data errors. External data needs validation. - A missing constraint. Without
extendsyou cannot access fields ofT— the compiler does not know they exist.