Notes on TypeScript Generics
Generics are the part of TypeScript that trips people up the most. They also unlock the most powerful patterns. Here are the parts I find useful and return to often.
The basic idea
A generic is a placeholder for a type, decided when the function or type is used — not when it is defined.
function identity<T>(value: T): T {
return value; // highlighted: T flows through unchanged
} // highlighted: return type matches input type
identity(42); // T is inferred as number
identity("hello"); // T is inferred as stringThe angle bracket <T> is the generic parameter. T is just a convention — you can call it anything.
Constraining generics
Without constraints, TypeScript knows very little about T. You can constrain it with extends:
function getLength<T extends { length: number }>(value: T): number {
return value.length;
}
getLength("hello"); // ✓
getLength([1, 2, 3]); // ✓
getLength(42); // ✗ Number has no .lengthThis is the key pattern: constrain to what you actually need, nothing more.
Generic interfaces and types
interface ApiResponse<T> {
data: T;
error: string | null;
status: number;
}
type UserResponse = ApiResponse<User>;
type PostsResponse = ApiResponse<Post[]>;Utility types (the ones I actually use)
TypeScript ships with generic utilities that are worth knowing:
| Utility | What it does |
|---|---|
Partial<T> | Makes all properties optional |
Required<T> | Makes all properties required |
Pick<T, K> | Selects a subset of keys |
Omit<T, K> | Removes a subset of keys |
Record<K, V> | Maps keys to a value type |
ReturnType<T> | Extracts a function's return type |
type User = {
id: number;
name: string;
email: string;
role: 'admin' | 'user';
};
// Only the fields needed for a form
type UserForm = Pick<User, 'name' | 'email'>;
// Update payload — everything optional
type UserUpdate = Partial<Pick<User, 'name' | 'email'>>;Conditional types
type IsArray<T> = T extends any[] ? true : false;
type A = IsArray<string[]>; // true
type B = IsArray<string>; // falseMore useful: infer lets you extract a type from within another:
type UnpackPromise<T> = T extends Promise<infer U> ? U : T;
type Resolved = UnpackPromise<Promise<string>>; // string
type Plain = UnpackPromise<number>; // numberThe thing that took me longest to understand
Generic parameters are not runtime values. They exist only at the type level and are erased in the compiled output. You cannot do this:
function create<T>(): T {
return new T(); // ✗ T is not available at runtime
}If you need runtime type information, you have to pass the class or a factory function as a value:
function create<T>(Ctor: new () => T): T {
return new Ctor();
}This small mental shift — understanding the compile-time/runtime divide — makes everything else click.