TypeScript Generics Deep Dive
Go beyond basic generics — constraints, conditional types, mapped types, infer, and real-world patterns for writing type-safe reusable code.
What you'll learn
- ✓How generic type parameters flow through functions, classes, and interfaces
- ✓Constraining generics with extends and keyof
- ✓Conditional types, mapped types, and the infer keyword
- ✓Real-world patterns: builders, factories, and type-safe event emitters
Prerequisites
- •Solid understanding of TypeScript basics (types, interfaces, unions)
- •Familiarity with basic generic syntax (
)
Generics let you write code that works with any type while preserving type safety. This guide covers the full spectrum — from constraints to conditional types to real patterns you will use in production.
Generic functions
A generic function declares a type parameter that the caller fills in (or TypeScript infers).
function identity<T>(value: T): T {
return value;
}
const num = identity(42); // T is number
const str = identity('hello'); // T is string
TypeScript infers T from the argument. You can also specify it explicitly:
const result = identity<string>('hello');
Multiple type parameters
function pair<A, B>(first: A, second: B): [A, B] {
return [first, second];
}
const p = pair('age', 30); // [string, number]
Constraints with extends
Unconstrained generics accept anything. Use extends to narrow the set of allowed types.
function getLength<T extends { length: number }>(item: T): number {
return item.length;
}
getLength('hello'); // OK — string has .length
getLength([1, 2, 3]); // OK — array has .length
getLength(42); // Error — number has no .length
keyof constraints
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: 'Alice', age: 30 };
const name = getProperty(user, 'name'); // string
const age = getProperty(user, 'age'); // number
getProperty(user, 'email'); // Error — 'email' is not in keyof typeof user
The return type T[K] is an indexed access type — it extracts the type of property K from T.
Generic interfaces and classes
interface Repository<T> {
findById(id: string): Promise<T | null>;
save(entity: T): Promise<T>;
delete(id: string): Promise<void>;
}
class UserRepo implements Repository<User> {
async findById(id: string): Promise<User | null> {
// ...
}
async save(user: User): Promise<User> {
// ...
}
async delete(id: string): Promise<void> {
// ...
}
}
Generic classes
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
peek(): T | undefined {
return this.items[this.items.length - 1];
}
}
const numbers = new Stack<number>();
numbers.push(1);
numbers.push(2);
const top = numbers.pop(); // number | undefined
Default type parameters
interface ApiResponse<T = unknown> {
data: T;
status: number;
message: string;
}
const generic: ApiResponse = { data: 'anything', status: 200, message: 'ok' };
const typed: ApiResponse<User[]> = { data: [], status: 200, message: 'ok' };
Conditional types
Conditional types select one of two types based on a condition.
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
Distributive conditional types
When T is a union, the condition distributes over each member:
type ToArray<T> = T extends unknown ? T[] : never;
type Result = ToArray<string | number>; // string[] | number[]
The infer keyword
infer lets you extract a type from within a pattern.
type ReturnOf<T> = T extends (...args: any[]) => infer R ? R : never;
type A = ReturnOf<() => string>; // string
type B = ReturnOf<(x: number) => boolean>; // boolean
Extract the element type of an array:
type ElementOf<T> = T extends (infer E)[] ? E : never;
type C = ElementOf<string[]>; // string
type D = ElementOf<number[]>; // number
Extract promise result:
type Awaited<T> = T extends Promise<infer R> ? Awaited<R> : T;
type E = Awaited<Promise<Promise<string>>>; // string
Mapped types
Mapped types iterate over keys to create new types.
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
type Optional<T> = {
[K in keyof T]?: T[K];
};
Remapping keys
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface Person {
name: string;
age: number;
}
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number }
Real-world patterns
Type-safe event emitter
type EventMap = {
login: { userId: string; timestamp: number };
logout: { userId: string };
error: { message: string; code: number };
};
class TypedEmitter<T extends Record<string, unknown>> {
private listeners: Partial<{
[K in keyof T]: Array<(payload: T[K]) => void>;
}> = {};
on<K extends keyof T>(event: K, handler: (payload: T[K]) => void): void {
const list = this.listeners[event] ?? [];
list.push(handler);
this.listeners[event] = list;
}
emit<K extends keyof T>(event: K, payload: T[K]): void {
this.listeners[event]?.forEach(fn => fn(payload));
}
}
const emitter = new TypedEmitter<EventMap>();
emitter.on('login', ({ userId, timestamp }) => {
// userId: string, timestamp: number — fully typed
});
emitter.emit('error', { message: 'oops', code: 500 });
Builder pattern
class QueryBuilder<T extends Record<string, unknown>> {
private conditions: string[] = [];
where<K extends keyof T>(
field: K,
op: '=' | '>' | '<',
value: T[K]
): this {
this.conditions.push(`${String(field)} ${op} ${JSON.stringify(value)}`);
return this;
}
build(): string {
return `SELECT * WHERE ${this.conditions.join(' AND ')}`;
}
}
interface Product {
name: string;
price: number;
inStock: boolean;
}
const query = new QueryBuilder<Product>()
.where('price', '>', 10)
.where('inStock', '=', true)
.build();
Summary
Generics are TypeScript’s most powerful feature for writing reusable, type-safe code. Start with simple type parameters, add constraints when you need to narrow what’s accepted, and reach for conditional types and infer when you need to transform or extract types. The patterns compound — once you are comfortable with the building blocks, you can model almost any API shape at the type level.
Related articles
- TypeScript TypeScript Generics: Advanced Patterns and Constraints
Master advanced TypeScript generics patterns including conditional constraints, recursive types, generic factories, and type-safe builder patterns with examples.
- TypeScript TypeScript Recursive Types Tutorial
Build recursive types in TypeScript: deep readonly, JSON, paths, and tuple manipulation. Learn how to write recursion that the compiler can actually evaluate.
- TypeScript TypeScript Utility Types: The Complete Guide
A comprehensive guide to all built-in TypeScript utility types including Partial, Required, Pick, Omit, Record, Extract, Exclude, and more with practical examples.
- TypeScript The Builder Pattern in TypeScript With Type Safety
Implement the builder pattern in TypeScript with compile-time enforcement of required fields using generics and conditional types.