Generics Deep Dive
Generics are the cornerstone of reusable, type-safe code in TypeScript. They allow you to write code that works with any type while maintaining full type information.1. Why Generics?
Consider this function:Copy
// Without generics - loses type information
function identity(value: any): any {
return value;
}
const result = identity('hello'); // type is 'any' - not helpful!
Copy
// With generics - type is preserved
function identity<T>(value: T): T {
return value;
}
const result = identity('hello'); // type is 'string'
const num = identity(42); // type is 'number'
2. Generic Functions
Basic Syntax
Copy
// Type parameter T
function wrap<T>(value: T): T[] {
return [value];
}
wrap('hello'); // string[]
wrap(42); // number[]
wrap(true); // boolean[]
// Explicit type argument
wrap<string>('hello'); // string[]
Multiple Type Parameters
Copy
function pair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
pair('hello', 42); // [string, number]
pair(true, { name: 'Alice' }); // [boolean, { name: string }]
// Map-like function
function mapPair<T, U, V>(pair: [T, U], fn: (a: T, b: U) => V): V {
return fn(pair[0], pair[1]);
}
mapPair([1, 2], (a, b) => a + b); // 3 (number)
Generic Arrow Functions
Copy
// Arrow function with generics
const identity = <T>(value: T): T => value;
// In TSX files, add a trailing comma to avoid JSX ambiguity
const identity2 = <T,>(value: T): T => value;
// With constraint
const getLength = <T extends { length: number }>(item: T): number => {
return item.length;
};
3. Generic Constraints
Restrict what types can be used with generics.extends Keyword
Copy
interface Lengthwise {
length: number;
}
function logLength<T extends Lengthwise>(item: T): number {
console.log(item.length);
return item.length;
}
logLength('hello'); // 5 - string has length
logLength([1, 2, 3]); // 3 - array has length
logLength({ length: 10 }); // 10 - object with length
// logLength(42); // ❌ Error: number doesn't have length
keyof Constraint
Copy
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: 'Alice', age: 25 };
getProperty(user, 'name'); // string
getProperty(user, 'age'); // number
// getProperty(user, 'email'); // ❌ Error: 'email' is not a key
Multiple Constraints
Copy
interface Printable {
print(): void;
}
interface Loggable {
log(): void;
}
function process<T extends Printable & Loggable>(item: T): void {
item.print();
item.log();
}
Conditional Constraints
Copy
type NumberOrString<T> = T extends number ? number : string;
function process<T extends number | string>(value: T): NumberOrString<T> {
if (typeof value === 'number') {
return (value * 2) as NumberOrString<T>;
}
return value.toUpperCase() as NumberOrString<T>;
}
const num = process(42); // number (84)
const str = process('hello'); // string ('HELLO')
4. Generic Classes
Classes that work with multiple types.Copy
class Container<T> {
private value: T;
constructor(value: T) {
this.value = value;
}
getValue(): T {
return this.value;
}
setValue(value: T): void {
this.value = value;
}
}
const stringContainer = new Container('hello');
console.log(stringContainer.getValue()); // 'hello' (string)
const numberContainer = new Container(42);
console.log(numberContainer.getValue()); // 42 (number)
Generic Stack
Copy
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];
}
isEmpty(): boolean {
return this.items.length === 0;
}
size(): number {
return this.items.length;
}
clear(): void {
this.items = [];
}
}
const stack = new Stack<number>();
stack.push(1);
stack.push(2);
stack.push(3);
console.log(stack.pop()); // 3
Generic with Static Members
Copy
class Registry<T> {
private static instances = new Map<string, any>();
private items = new Map<string, T>();
register(key: string, item: T): void {
this.items.set(key, item);
}
get(key: string): T | undefined {
return this.items.get(key);
}
static getInstance<U>(name: string): Registry<U> {
if (!this.instances.has(name)) {
this.instances.set(name, new Registry<U>());
}
return this.instances.get(name);
}
}
5. Generic Interfaces
Copy
// Generic interface
interface Repository<T> {
find(id: number): T | undefined;
findAll(): T[];
create(item: Omit<T, 'id'>): T;
update(id: number, item: Partial<T>): T | undefined;
delete(id: number): boolean;
}
// Generic with multiple parameters
interface Result<T, E> {
success: boolean;
data?: T;
error?: E;
}
// Extending generic interface
interface TimestampedRepository<T> extends Repository<T> {
findByDate(start: Date, end: Date): T[];
}
Implementing Generic Interfaces
Copy
interface Entity {
id: number;
}
interface User extends Entity {
name: string;
email: string;
}
class UserRepository implements Repository<User> {
private users: User[] = [];
private nextId = 1;
find(id: number): User | undefined {
return this.users.find(u => u.id === id);
}
findAll(): User[] {
return [...this.users];
}
create(item: Omit<User, 'id'>): User {
const user = { ...item, id: this.nextId++ };
this.users.push(user);
return user;
}
update(id: number, item: Partial<User>): User | undefined {
const index = this.users.findIndex(u => u.id === id);
if (index === -1) return undefined;
this.users[index] = { ...this.users[index], ...item };
return this.users[index];
}
delete(id: number): boolean {
const index = this.users.findIndex(u => u.id === id);
if (index === -1) return false;
this.users.splice(index, 1);
return true;
}
}
6. Default Type Parameters
Provide defaults for generic parameters.Copy
// Default type parameter
interface Response<T = any> {
data: T;
status: number;
message: string;
}
const response1: Response = { data: 'anything', status: 200, message: 'OK' };
const response2: Response<User> = { data: user, status: 200, message: 'OK' };
// Multiple defaults
interface Paginated<T = any, M = Record<string, unknown>> {
items: T[];
total: number;
page: number;
metadata: M;
}
// Defaults with constraints
interface Container<T extends object = object> {
value: T;
}
7. Generic Type Aliases
Copy
// Generic type alias
type Nullable<T> = T | null | undefined;
type MaybeString = Nullable<string>; // string | null | undefined
// Complex generic type
type AsyncResult<T> = {
loading: boolean;
error: Error | null;
data: T | null;
};
type UserResult = AsyncResult<User>;
// Recursive generic
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};
// Generic function type
type Mapper<T, U> = (item: T) => U;
const toUpper: Mapper<string, string> = (s) => s.toUpperCase();
const toLength: Mapper<string, number> = (s) => s.length;
8. Generic Utility Patterns
Factory Pattern
Copy
interface Constructor<T> {
new (...args: any[]): T;
}
function createInstance<T>(ctor: Constructor<T>, ...args: any[]): T {
return new ctor(...args);
}
class User {
constructor(public name: string) {}
}
const user = createInstance(User, 'Alice'); // User
Builder Pattern
Copy
class QueryBuilder<T> {
private query: Partial<T> = {};
where<K extends keyof T>(key: K, value: T[K]): this {
this.query[key] = value;
return this;
}
build(): Partial<T> {
return { ...this.query };
}
}
interface User {
id: number;
name: string;
email: string;
role: 'admin' | 'user';
}
const query = new QueryBuilder<User>()
.where('role', 'admin')
.where('name', 'Alice')
.build();
// { role: 'admin', name: 'Alice' }
Result/Either Pattern
Copy
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
function divide(a: number, b: number): Result<number, string> {
if (b === 0) {
return { ok: false, error: 'Division by zero' };
}
return { ok: true, value: a / b };
}
const result = divide(10, 2);
if (result.ok) {
console.log(result.value); // 5
} else {
console.error(result.error);
}
Option/Maybe Pattern
Copy
class Option<T> {
private constructor(private value: T | null) {}
static some<T>(value: T): Option<T> {
return new Option(value);
}
static none<T>(): Option<T> {
return new Option<T>(null);
}
map<U>(fn: (value: T) => U): Option<U> {
if (this.value === null) return Option.none();
return Option.some(fn(this.value));
}
flatMap<U>(fn: (value: T) => Option<U>): Option<U> {
if (this.value === null) return Option.none();
return fn(this.value);
}
getOrElse(defaultValue: T): T {
return this.value ?? defaultValue;
}
isSome(): boolean {
return this.value !== null;
}
}
function findUser(id: number): Option<User> {
const user = users.find(u => u.id === id);
return user ? Option.some(user) : Option.none();
}
const userName = findUser(1)
.map(user => user.name)
.getOrElse('Unknown');
9. Advanced Generic Patterns
Variadic Tuple Types
Copy
// Concatenate tuples
type Concat<T extends any[], U extends any[]> = [...T, ...U];
type A = Concat<[1, 2], [3, 4]>; // [1, 2, 3, 4]
// Function composition
function pipe<A extends any[], B, C>(
fn1: (...args: A) => B,
fn2: (arg: B) => C
): (...args: A) => C {
return (...args) => fn2(fn1(...args));
}
const addOne = (x: number) => x + 1;
const double = (x: number) => x * 2;
const addOneThenDouble = pipe(addOne, double);
console.log(addOneThenDouble(5)); // 12
Recursive Generics
Copy
// Flatten nested arrays
type Flatten<T> = T extends Array<infer U> ? Flatten<U> : T;
type Nested = number[][][];
type Flat = Flatten<Nested>; // number
// Deep object paths
type PathKeys<T, Prefix extends string = ''> = T extends object
? {
[K in keyof T]: K extends string
? Prefix extends ''
? K | `${K}.${PathKeys<T[K], K>}`
: `${Prefix}.${K}` | `${Prefix}.${K}.${PathKeys<T[K], K>}`
: never;
}[keyof T]
: never;
interface User {
name: string;
address: {
city: string;
country: {
code: string;
};
};
}
type UserPaths = PathKeys<User>;
// 'name' | 'address' | 'address.city' | 'address.country' | 'address.country.code'
Conditional Generics
Copy
// Different return types based on input
type ArrayOrSingle<T, Multi extends boolean> = Multi extends true
? T[]
: T;
function fetch<T, Multi extends boolean = false>(
url: string,
options?: { multi?: Multi }
): Promise<ArrayOrSingle<T, Multi>> {
// Implementation
return {} as any;
}
const single = await fetch<User>('/user/1'); // User
const multiple = await fetch<User>('/users', { multi: true }); // User[]
10. Real-World Examples
Type-Safe Event Emitter
Copy
type EventMap = Record<string, any>;
type EventKey<T extends EventMap> = string & keyof T;
type EventCallback<T> = (data: T) => void;
class EventEmitter<T extends EventMap> {
private listeners = new Map<keyof T, Set<EventCallback<any>>>();
on<K extends EventKey<T>>(event: K, callback: EventCallback<T[K]>): () => void {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event)!.add(callback);
// Return unsubscribe function
return () => this.off(event, callback);
}
off<K extends EventKey<T>>(event: K, callback: EventCallback<T[K]>): void {
this.listeners.get(event)?.delete(callback);
}
emit<K extends EventKey<T>>(event: K, data: T[K]): void {
this.listeners.get(event)?.forEach((callback) => callback(data));
}
}
// Usage
interface AppEvents {
login: { userId: string };
logout: { timestamp: Date };
error: { message: string; code: number };
}
const events = new EventEmitter<AppEvents>();
const unsubscribe = events.on('login', (data) => {
console.log(`User ${data.userId} logged in`);
});
events.emit('login', { userId: '123' }); // Type-safe!
unsubscribe(); // Clean up
Type-Safe HTTP Client
Copy
interface ApiEndpoints {
'/users': {
GET: { response: User[] };
POST: { body: Omit<User, 'id'>; response: User };
};
'/users/:id': {
GET: { response: User };
PUT: { body: Partial<User>; response: User };
DELETE: { response: void };
};
}
type Method = 'GET' | 'POST' | 'PUT' | 'DELETE';
type PathParams<P extends string> = P extends `${string}:${infer Param}/${infer Rest}`
? { [K in Param | keyof PathParams<Rest>]: string }
: P extends `${string}:${infer Param}`
? { [K in Param]: string }
: {};
class HttpClient<T extends Record<string, Record<Method, any>>> {
async request<
P extends keyof T & string,
M extends keyof T[P] & Method
>(
method: M,
path: P,
options?: {
params?: PathParams<P>;
body?: T[P][M] extends { body: infer B } ? B : never;
}
): Promise<T[P][M] extends { response: infer R } ? R : never> {
// Implementation
return {} as any;
}
}
const client = new HttpClient<ApiEndpoints>();
// Fully type-safe API calls
const users = await client.request('GET', '/users');
const newUser = await client.request('POST', '/users', {
body: { name: 'Alice', email: '[email protected]' }
});
Summary
| Concept | Example |
|---|---|
| Basic Generic | function identity<T>(val: T): T |
| Multiple Parameters | function pair<T, U>(a: T, b: U): [T, U] |
| Constraints | <T extends { length: number }> |
| keyof Constraint | <K extends keyof T> |
| Generic Class | class Stack<T> { items: T[] } |
| Generic Interface | interface Repo<T> { find(id): T } |
| Default Parameter | interface Response<T = any> |
| Conditional | T extends U ? X : Y |
| infer | T extends Array<infer U> ? U : T |
| Variadic Tuples | [...T, ...U] |