TypeScript generics separate professional code from stringly-typed chaos. They let you write flexible, reusable components while keeping type safety. This guide covers the patterns you’ll actually use — not academic type gymnastics.
- Generics add type parameters to functions, interfaces, and classes:
<T> - Constraints limit what types can be used:
<T extends { id: string }> - Conditional types create type branches:
T extends U ? X : Y - Mapped types transform object properties:
{ [K in keyof T]: V } - Utility types (Pick, Omit, Partial, Required) cover 80% of use cases
inferkeyword extracts types from complex structures
Basic Generics
Generic Functions
// Without generics — loses type information
function identity(arg: any): any {
return arg;
}
// With generics — preserves type
function identity<T>(arg: T): T {
return arg;
}
// Usage
const num = identity<number>(42); // Type: number
const str = identity<string>('hello'); // Type: string
const inferred = identity([1, 2, 3]); // Type: number[] (inferred) Generic Interfaces
interface ApiResponse<T> {
data: T;
status: number;
error?: string;
}
// Usage
interface User {
id: string;
name: string;
}
const userResponse: ApiResponse<User> = {
data: { id: '1', name: 'Alice' },
status: 200
};
const listResponse: ApiResponse<User[]> = {
data: [{ id: '1', name: 'Alice' }],
status: 200
}; Generic Classes
class Queue<T> {
private items: T[] = [];
enqueue(item: T): void {
this.items.push(item);
}
dequeue(): T | undefined {
return this.items.shift();
}
peek(): T | undefined {
return this.items[0];
}
size(): number {
return this.items.length;
}
}
// Usage
const stringQueue = new Queue<string>();
stringQueue.enqueue('first');
stringQueue.enqueue('second');
const numberQueue = new Queue<number>();
numberQueue.enqueue(1);
numberQueue.enqueue(2); Real-World Patterns
1. Repository Pattern with Generics
interface Entity {
id: string;
createdAt: Date;
updatedAt: Date;
}
class Repository<T extends Entity> {
private items: Map<string, T> = new Map();
create(item: Omit<T, 'id' | 'createdAt' | 'updatedAt'>): T {
const now = new Date();
const newItem = {
...item,
id: crypto.randomUUID(),
createdAt: now,
updatedAt: now
} as T;
this.items.set(newItem.id, newItem);
return newItem;
}
findById(id: string): T | undefined {
return this.items.get(id);
}
findAll(): T[] {
return Array.from(this.items.values());
}
update(id: string, updates: Partial<Omit<T, 'id' | 'createdAt'>>): T | undefined {
const existing = this.items.get(id);
if (!existing) return undefined;
const updated = {
...existing,
...updates,
updatedAt: new Date()
};
this.items.set(id, updated);
return updated;
}
delete(id: string): boolean {
return this.items.delete(id);
}
}
// Usage
interface Product extends Entity {
name: string;
price: number;
inStock: boolean;
}
const productRepo = new Repository<Product>();
const product = productRepo.create({
name: 'Laptop',
price: 999,
inStock: true
});
const updated = productRepo.update(product.id, { price: 899 }); 2. API Client with Type Safety
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
interface RequestConfig<TBody = unknown> {
method: HttpMethod;
url: string;
body?: TBody;
headers?: Record<string, string>;
}
interface ApiResponse<T> {
data: T;
status: number;
headers: Headers;
}
class ApiClient {
private baseUrl: string;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
async request<TResponse, TBody = unknown>(
config: RequestConfig<TBody>
): Promise<ApiResponse<TResponse>> {
const response = await fetch(`${this.baseUrl}${config.url}`, {
method: config.method,
headers: {
'Content-Type': 'application/json',
...config.headers
},
body: config.body ? JSON.stringify(config.body) : undefined
});
const data = await response.json();
return {
data,
status: response.status,
headers: response.headers
};
}
// Convenience methods
get<T>(url: string) {
return this.request<T>({ method: 'GET', url });
}
post<T, B>(url: string, body: B) {
return this.request<T, B>({ method: 'POST', url, body });
}
put<T, B>(url: string, body: B) {
return this.request<T, B>({ method: 'PUT', url, body });
}
patch<T, B>(url: string, body: B) {
return this.request<T, B>({ method: 'PATCH', url, body });
}
delete<T>(url: string) {
return this.request<T>({ method: 'DELETE', url });
}
}
// Usage
interface User {
id: string;
email: string;
name: string;
}
interface CreateUserDto {
email: string;
name: string;
}
const api = new ApiClient('https://api.example.com');
// Fully typed API calls
const { data: users } = await api.get<User[]>('/users');
const { data: newUser } = await api.post<User, CreateUserDto>('/users', {
email: 'alice@example.com',
name: 'Alice'
});
const { data: updated } = await api.patch<User, Partial<User>>(
`/users/${newUser.id}`,
{ name: 'Alice Smith' }
); 3. Event Emitter with Type Safety
type EventMap = Record<string, any>;
type EventKey<T extends EventMap> = string & keyof T;
type EventHandler<T> = (payload: T) => void;
class TypedEventEmitter<Events extends EventMap> {
private handlers: {
[K in keyof Events]?: EventHandler<Events[K]>[]
} = {};
on<K extends EventKey<Events>>(
event: K,
handler: EventHandler<Events[K]>
): () => void {
if (!this.handlers[event]) {
this.handlers[event] = [];
}
this.handlers[event]!.push(handler);
// Return unsubscribe function
return () => this.off(event, handler);
}
off<K extends EventKey<Events>>(
event: K,
handler: EventHandler<Events[K]>
): void {
const handlers = this.handlers[event];
if (handlers) {
const index = handlers.indexOf(handler);
if (index > -1) {
handlers.splice(index, 1);
}
}
}
emit<K extends EventKey<Events>>(event: K, payload: Events[K]): void {
const handlers = this.handlers[event];
if (handlers) {
handlers.forEach(h => h(payload));
}
}
}
// Usage
interface MyEvents {
'user:login': { userId: string; timestamp: Date };
'user:logout': { userId: string };
'data:update': { table: string; records: number };
'error': { message: string; code: number };
}
const emitter = new TypedEventEmitter<MyEvents>();
// Type-safe event handling
emitter.on('user:login', ({ userId, timestamp }) => {
console.log(`User ${userId} logged in at ${timestamp}`);
});
emitter.on('data:update', ({ table, records }) => {
console.log(`${table} updated with ${records} records`);
});
// Type error: wrong payload shape
// emitter.emit('user:login', { userId: '1' }); // Error: missing timestamp
// Correct usage
emitter.emit('user:login', {
userId: 'user-123',
timestamp: new Date()
}); Advanced Patterns
4. Constrained Generics
// Only accept types with an 'id' property
interface HasId {
id: string;
}
function findById<T extends HasId>(items: T[], id: string): T | undefined {
return items.find(item => item.id === id);
}
// Multiple constraints
interface HasTimestamps {
createdAt: Date;
updatedAt: Date;
}
function sortByDate<T extends HasTimestamps>(items: T[]): T[] {
return [...items].sort((a, b) =>
b.createdAt.getTime() - a.createdAt.getTime()
);
}
// Keyof constraints
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: '1', name: 'Alice', age: 30 };
const name = getProperty(user, 'name'); // Type: string
const age = getProperty(user, 'age'); // Type: number
// const wrong = getProperty(user, 'email'); // Error: 'email' doesn't exist 5. Conditional Types
// Basic conditional type
type IsString<T> = T extends string ? true : false;
type A = IsString<'hello'>; // true
type B = IsString<123>; // false
// Extract type from array
type ElementType<T> = T extends (infer E)[] ? E : T;
type Numbers = ElementType<number[]>; // number
type StringOrNum = ElementType<string>; // string
// Flatten nested arrays
type Flatten<T> = T extends (infer E)[] ? Flatten<E> : T;
type DeepArray = Flatten<string[][][]>; // string
// Create nullable version
type Nullable<T> = T | null;
type NullableString = Nullable<string>; // string | null
// Non-null type
type NonNullable<T> = T extends null | undefined ? never : T;
type DefinitelyString = NonNullable<string | null>; // string 6. Mapped Types
// Make all properties optional
type Partial<T> = {
[P in keyof T]?: T[P];
};
// Make all properties required
type Required<T> = {
[P in keyof T]-?: T[P];
};
// Make all properties readonly
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
// Remove readonly
type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};
// Pick specific properties
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
// Omit specific properties
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
// Record type
type Record<K extends keyof any, T> = {
[P in K]: T;
};
// Usage examples
interface User {
id: string;
email: string;
name: string;
password: string;
createdAt: Date;
}
// API response doesn't include password
type UserResponse = Omit<User, 'password'>;
// Form input only needs name and email
type UserFormInput = Pick<User, 'name' | 'email'>;
// Update can have partial fields
type UserUpdate = Partial<Omit<User, 'id' | 'createdAt'>>;
// Readonly for immutable state
type ImmutableUser = Readonly<User>; 7. Template Literal Types
// Create event names from actions
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<'click'>; // 'onClick'
type HoverEvent = EventName<'hover'>; // 'onHover'
// CSS property types
type CSSProperty = 'margin' | 'padding' | 'border';
type CSSDirection = 'top' | 'right' | 'bottom' | 'left';
type CSSPropertyWithDirection = `${CSSProperty}${Capitalize<CSSDirection>}`;
// 'marginTop' | 'marginRight' | 'marginBottom' | 'marginLeft'
// 'paddingTop' | 'paddingRight' | ... etc
// Route parameters
type Route<Path extends string> =
Path extends `${infer Start}/:${infer Param}/${infer Rest}`
? { [K in Param]: string } & Route<`${Start}/${Rest}`>
: Path extends `${string}/:${infer Param}`
? { [K in Param]: string }
: {};
// Usage
type UserRoute = Route<'/users/:userId/posts/:postId'>;
// { userId: string; postId: string } 8. Infer with Generics
// Extract return type
type ReturnType<T extends (...args: any[]) => any> =
T extends (...args: any[]) => infer R ? R : never;
function createUser() {
return { id: '1', name: 'Alice' };
}
type User = ReturnType<typeof createUser>;
// { id: string; name: string }
// Extract parameters
type Parameters<T extends (...args: any[]) => any> =
T extends (...args: infer P) => any ? P : never;
function updateUser(id: string, data: Partial<User>) {
return { ...data, id };
}
type UpdateUserParams = Parameters<typeof updateUser>;
// [string, Partial<User>]
// Extract Promise type
type Awaited<T> = T extends Promise<infer U> ? U : T;
async function fetchUser(): Promise<User> {
return { id: '1', name: 'Alice' };
}
type FetchedUser = Awaited<ReturnType<typeof fetchUser>>;
// User (not Promise<User>) Utility Types Reference
| Type | Purpose | Example |
|---|---|---|
Partial<T> | All properties optional | Partial<User> |
Required<T> | All properties required | Required<Config> |
Readonly<T> | All properties readonly | Readonly<State> |
Pick<T, K> | Select specific keys | Pick<User, 'id' | 'name'> |
Omit<T, K> | Remove specific keys | Omit<User, 'password'> |
Record<K, V> | Dictionary type | Record<string, User> |
Exclude<T, U> | Remove types from union | Exclude<'a' | 'b', 'a'> |
Extract<T, U> | Extract types from union | Extract<'a' | 'b', 'a'> |
NonNullable<T> | Remove null/undefined | NonNullable<string | null> |
ReturnType<T> | Function return type | ReturnType<typeof fn> |
Parameters<T> | Function parameters | Parameters<typeof fn> |
Awaited<T> | Unwrap Promise | Awaited<Promise<User>> |
Common Patterns
Factory Pattern
interface EntityConstructor<T extends HasId> {
new (data: Omit<T, 'id'>): T;
}
function createEntity<T extends HasId>(
Constructor: EntityConstructor<T>,
data: Omit<T, 'id'>
): T {
return new Constructor(data);
}
class Product implements HasId {
id: string;
name: string;
price: number;
constructor(data: Omit<Product, 'id'>) {
this.id = crypto.randomUUID();
this.name = data.name;
this.price = data.price;
}
}
const product = createEntity(Product, { name: 'Laptop', price: 999 }); Builder Pattern
class QueryBuilder<T extends Record<string, any>> {
private filters: Partial<T> = {};
private sortKey: keyof T | null = null;
private sortDirection: 'asc' | 'desc' = 'asc';
where<K extends keyof T>(key: K, value: T[K]): this {
this.filters[key] = value;
return this;
}
orderBy(key: keyof T, direction: 'asc' | 'desc' = 'asc'): this {
this.sortKey = key;
this.sortDirection = direction;
return this;
}
build(): { filters: Partial<T>; sort: { key: keyof T; direction: 'asc' | 'desc' } | null } {
return {
filters: this.filters,
sort: this.sortKey ? { key: this.sortKey, direction: this.sortDirection } : null
};
}
}
interface User {
id: string;
name: string;
age: number;
active: boolean;
}
const query = new QueryBuilder<User>()
.where('active', true)
.where('age', 25)
.orderBy('name', 'asc')
.build(); Summary
- Generics add flexibility while preserving type safety
- Constraints (
extends) ensure types have required properties - Conditional types create type-level logic
- Mapped types transform object shapes
- Utility types solve common transformation needs
- Template literals create type-safe string patterns
Master these patterns and you’ll write TypeScript that’s both flexible and bulletproof.
What to Read Next
- TypeScript Cheat Sheet — Quick reference for all TypeScript syntax
- React with TypeScript — Type-safe React patterns
- Node.js with TypeScript — Backend TypeScript setup
Related Articles
Deepen your understanding with these curated continuations.
TypeScript Utility Types: Pick, Omit, Partial & Deep Dive
Master TypeScript's built-in utility types like Pick, Omit, and Partial. Learn real production patterns for Required, Record, and function-derived types.
TypeScript Cheat Sheet: Types, Generics & Utilities
The essential TypeScript reference for primitive types, generics, and utility types. Learn type guards, mapped types, and optimal tsconfig configurations.
Next.js App Router Cheat Sheet (Next.js 15, 2026)
Complete Next.js App Router reference — file conventions, Server vs Client Components, data fetching, Server Actions, metadata, and Pages Router migration table.