Generics enable reusable, type-safe code. They let you write functions and classes that work with any type while maintaining type information.
Basic Generics#
1// Without generics - loses type information
2function firstElement(arr: any[]): any {
3 return arr[0];
4}
5
6// With generics - preserves type
7function firstElement<T>(arr: T[]): T | undefined {
8 return arr[0];
9}
10
11const num = firstElement([1, 2, 3]); // number
12const str = firstElement(['a', 'b', 'c']); // string
13
14// Multiple type parameters
15function pair<T, U>(first: T, second: U): [T, U] {
16 return [first, second];
17}
18
19const p = pair('hello', 42); // [string, number]Generic Constraints#
1// Constrain to types with specific properties
2interface HasLength {
3 length: number;
4}
5
6function logLength<T extends HasLength>(item: T): T {
7 console.log(item.length);
8 return item;
9}
10
11logLength('hello'); // OK - string has length
12logLength([1, 2, 3]); // OK - array has length
13logLength({ length: 10 }); // OK - object has length
14// logLength(123); // Error - number doesn't have length
15
16// Constrain to object keys
17function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
18 return obj[key];
19}
20
21const person = { name: 'Alice', age: 30 };
22const name = getProperty(person, 'name'); // string
23const age = getProperty(person, 'age'); // number
24// getProperty(person, 'email'); // Error - 'email' not in personGeneric Interfaces and Types#
1// Generic interface
2interface Response<T> {
3 data: T;
4 status: number;
5 message: string;
6}
7
8interface User {
9 id: string;
10 name: string;
11}
12
13const userResponse: Response<User> = {
14 data: { id: '1', name: 'Alice' },
15 status: 200,
16 message: 'Success',
17};
18
19// Generic type alias
20type Result<T, E = Error> =
21 | { success: true; data: T }
22 | { success: false; error: E };
23
24function divide(a: number, b: number): Result<number, string> {
25 if (b === 0) {
26 return { success: false, error: 'Division by zero' };
27 }
28 return { success: true, data: a / b };
29}
30
31// Generic with default type
32interface Container<T = string> {
33 value: T;
34}
35
36const strContainer: Container = { value: 'hello' }; // T is string
37const numContainer: Container<number> = { value: 42 };Generic Classes#
1class Stack<T> {
2 private items: T[] = [];
3
4 push(item: T): void {
5 this.items.push(item);
6 }
7
8 pop(): T | undefined {
9 return this.items.pop();
10 }
11
12 peek(): T | undefined {
13 return this.items[this.items.length - 1];
14 }
15
16 isEmpty(): boolean {
17 return this.items.length === 0;
18 }
19}
20
21const numberStack = new Stack<number>();
22numberStack.push(1);
23numberStack.push(2);
24const top = numberStack.pop(); // number | undefined
25
26// Generic class with constraints
27class KeyValueStore<K extends string | number, V> {
28 private store = new Map<K, V>();
29
30 set(key: K, value: V): void {
31 this.store.set(key, value);
32 }
33
34 get(key: K): V | undefined {
35 return this.store.get(key);
36 }
37}
38
39const store = new KeyValueStore<string, User>();
40store.set('user1', { id: '1', name: 'Alice' });Utility Types with Generics#
1// Built-in utility types use generics
2
3// Partial - make all properties optional
4type PartialUser = Partial<User>;
5// { id?: string; name?: string; }
6
7// Required - make all properties required
8type RequiredUser = Required<PartialUser>;
9
10// Pick - select specific properties
11type UserName = Pick<User, 'name'>;
12// { name: string; }
13
14// Omit - exclude properties
15type UserWithoutId = Omit<User, 'id'>;
16// { name: string; }
17
18// Record - create object type with keys and values
19type UserMap = Record<string, User>;
20
21// Custom utility types
22type Nullable<T> = T | null;
23type Optional<T> = T | undefined;
24
25type DeepPartial<T> = {
26 [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
27};
28
29type DeepReadonly<T> = {
30 readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
31};Generic Functions in Practice#
1// API response handler
2async function fetchData<T>(url: string): Promise<T> {
3 const response = await fetch(url);
4 if (!response.ok) {
5 throw new Error(`HTTP error: ${response.status}`);
6 }
7 return response.json() as Promise<T>;
8}
9
10const users = await fetchData<User[]>('/api/users');
11const post = await fetchData<Post>('/api/posts/1');
12
13// Type-safe event emitter
14class TypedEventEmitter<Events extends Record<string, any>> {
15 private listeners = new Map<keyof Events, Set<Function>>();
16
17 on<K extends keyof Events>(event: K, listener: (data: Events[K]) => void): void {
18 if (!this.listeners.has(event)) {
19 this.listeners.set(event, new Set());
20 }
21 this.listeners.get(event)!.add(listener);
22 }
23
24 emit<K extends keyof Events>(event: K, data: Events[K]): void {
25 this.listeners.get(event)?.forEach((listener) => listener(data));
26 }
27}
28
29interface AppEvents {
30 userLogin: { userId: string; timestamp: Date };
31 orderPlaced: { orderId: string; total: number };
32}
33
34const emitter = new TypedEventEmitter<AppEvents>();
35
36emitter.on('userLogin', (data) => {
37 console.log(data.userId); // TypeScript knows this is string
38});
39
40emitter.emit('orderPlaced', { orderId: '123', total: 99.99 });Conditional Types#
1// Type that depends on condition
2type IsString<T> = T extends string ? true : false;
3
4type A = IsString<string>; // true
5type B = IsString<number>; // false
6
7// Extract types from generics
8type ArrayElement<T> = T extends (infer E)[] ? E : never;
9
10type NumArray = ArrayElement<number[]>; // number
11type StrArray = ArrayElement<string[]>; // string
12
13// Function return type
14type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
15
16function greet(name: string): string {
17 return `Hello, ${name}`;
18}
19
20type GreetReturn = ReturnType<typeof greet>; // string
21
22// Exclude and Extract
23type Exclude<T, U> = T extends U ? never : T;
24type Extract<T, U> = T extends U ? T : never;
25
26type Numbers = 1 | 2 | 3 | 4 | 5;
27type SmallNumbers = Extract<Numbers, 1 | 2>; // 1 | 2
28type LargeNumbers = Exclude<Numbers, 1 | 2>; // 3 | 4 | 5Mapped Types#
1// Transform all properties
2type Readonly<T> = {
3 readonly [P in keyof T]: T[P];
4};
5
6type Mutable<T> = {
7 -readonly [P in keyof T]: T[P];
8};
9
10// Add suffix to all keys
11type Getters<T> = {
12 [P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
13};
14
15interface Person {
16 name: string;
17 age: number;
18}
19
20type PersonGetters = Getters<Person>;
21// { getName: () => string; getAge: () => number; }
22
23// Filter properties by type
24type StringKeys<T> = {
25 [K in keyof T]: T[K] extends string ? K : never;
26}[keyof T];
27
28type PersonStringKeys = StringKeys<Person>; // 'name'Best Practices#
1// DO: Use descriptive type parameter names for complex generics
2interface Repository<TEntity, TId = string> {
3 findById(id: TId): Promise<TEntity | null>;
4 save(entity: TEntity): Promise<TEntity>;
5}
6
7// DO: Constrain when you need specific properties
8function merge<T extends object, U extends object>(a: T, b: U): T & U {
9 return { ...a, ...b };
10}
11
12// DON'T: Over-constrain
13// Bad: Too restrictive
14function bad<T extends { id: string; name: string; email: string }>(obj: T) {}
15
16// Good: Only require what you need
17function good<T extends { id: string }>(obj: T) {}
18
19// DO: Use defaults for common cases
20interface ApiResponse<T, E = Error> {
21 data?: T;
22 error?: E;
23}Conclusion#
Generics are essential for writing reusable, type-safe TypeScript code. Start with simple type parameters, add constraints when needed, and use utility types to transform existing types. The type system becomes a powerful tool for catching errors at compile time.