Generics allow you to create reusable components that work with multiple types while maintaining type safety.
Basic Generic Syntax#
1// Generic function
2function identity<T>(value: T): T {
3 return value;
4}
5
6// TypeScript infers the type
7identity('hello'); // T is string
8identity(42); // T is number
9identity(true); // T is boolean
10
11// Explicit type argument
12identity<string>('hello');
13identity<number>(42);
14
15// Generic arrow function
16const identityArrow = <T>(value: T): T => value;
17
18// Note: In JSX files, use trailing comma
19const identityJSX = <T,>(value: T): T => value;Generic Functions#
1// Swap function
2function swap<T, U>(pair: [T, U]): [U, T] {
3 return [pair[1], pair[0]];
4}
5
6swap([1, 'hello']); // [string, number]
7swap(['a', true]); // [boolean, string]
8
9// First element
10function first<T>(array: T[]): T | undefined {
11 return array[0];
12}
13
14first([1, 2, 3]); // number | undefined
15first(['a', 'b']); // string | undefined
16
17// Map function
18function map<T, U>(array: T[], fn: (item: T) => U): U[] {
19 return array.map(fn);
20}
21
22map([1, 2, 3], n => n.toString()); // string[]
23map(['a', 'b'], s => s.length); // number[]Generic Interfaces#
1// Generic interface
2interface Container<T> {
3 value: T;
4 getValue(): T;
5}
6
7const stringContainer: Container<string> = {
8 value: 'hello',
9 getValue() { return this.value; }
10};
11
12const numberContainer: Container<number> = {
13 value: 42,
14 getValue() { return this.value; }
15};
16
17// Generic interface with methods
18interface Repository<T> {
19 getById(id: string): T | undefined;
20 getAll(): T[];
21 save(item: T): void;
22 delete(id: string): boolean;
23}
24
25// Implementation
26interface User {
27 id: string;
28 name: string;
29}
30
31class UserRepository implements Repository<User> {
32 private users: User[] = [];
33
34 getById(id: string): User | undefined {
35 return this.users.find(u => u.id === id);
36 }
37
38 getAll(): User[] {
39 return [...this.users];
40 }
41
42 save(user: User): void {
43 this.users.push(user);
44 }
45
46 delete(id: string): boolean {
47 const index = this.users.findIndex(u => u.id === id);
48 if (index >= 0) {
49 this.users.splice(index, 1);
50 return true;
51 }
52 return false;
53 }
54}Generic Classes#
1class Box<T> {
2 private value: T;
3
4 constructor(value: T) {
5 this.value = value;
6 }
7
8 getValue(): T {
9 return this.value;
10 }
11
12 setValue(value: T): void {
13 this.value = value;
14 }
15}
16
17const stringBox = new Box('hello');
18stringBox.getValue(); // string
19
20const numberBox = new Box(42);
21numberBox.getValue(); // number
22
23// Multiple type parameters
24class Pair<T, U> {
25 constructor(
26 public first: T,
27 public second: U
28 ) {}
29
30 swap(): Pair<U, T> {
31 return new Pair(this.second, this.first);
32 }
33}
34
35const pair = new Pair('hello', 42);
36const swapped = pair.swap(); // Pair<number, string>Generic Constraints#
1// Constrain to types with length
2interface HasLength {
3 length: number;
4}
5
6function logLength<T extends HasLength>(item: T): number {
7 console.log(item.length);
8 return item.length;
9}
10
11logLength('hello'); // OK - string has length
12logLength([1, 2, 3]); // OK - array has length
13// logLength(42); // Error - number has no length
14
15// Constrain to object types
16function merge<T extends object, U extends object>(a: T, b: U): T & U {
17 return { ...a, ...b };
18}
19
20merge({ name: 'John' }, { age: 30 });
21// merge('hello', 'world'); // Error
22
23// Constrain to keys of another type
24function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
25 return obj[key];
26}
27
28const user = { name: 'John', age: 30 };
29getProperty(user, 'name'); // string
30getProperty(user, 'age'); // number
31// getProperty(user, 'email'); // Error - 'email' not in keyof userDefault Type Parameters#
1// Default type
2interface ApiResponse<T = unknown> {
3 data: T;
4 status: number;
5 message: string;
6}
7
8// Uses default (unknown)
9const response1: ApiResponse = {
10 data: 'anything',
11 status: 200,
12 message: 'OK'
13};
14
15// Explicit type
16const response2: ApiResponse<User> = {
17 data: { id: '1', name: 'John' },
18 status: 200,
19 message: 'OK'
20};
21
22// Multiple defaults
23interface Config<T = string, U = number> {
24 value: T;
25 count: U;
26}
27
28const config1: Config = { value: 'hello', count: 5 };
29const config2: Config<boolean> = { value: true, count: 5 };
30const config3: Config<boolean, string> = { value: true, count: 'five' };Generic Type Aliases#
1// Simple alias
2type Nullable<T> = T | null;
3type Optional<T> = T | undefined;
4
5const name: Nullable<string> = null;
6const age: Optional<number> = undefined;
7
8// Array wrapper
9type ArrayOf<T> = T[];
10const numbers: ArrayOf<number> = [1, 2, 3];
11
12// Object with value
13type ValueHolder<T> = { value: T };
14const holder: ValueHolder<string> = { value: 'hello' };
15
16// Function type
17type Mapper<T, U> = (item: T) => U;
18const toString: Mapper<number, string> = n => n.toString();
19
20// Conditional generic
21type NonNullable<T> = T extends null | undefined ? never : T;
22type Result = NonNullable<string | null>; // stringGeneric Utility Types#
1// Built-in utility types use generics
2
3// Partial - make all properties optional
4interface User {
5 id: string;
6 name: string;
7 email: string;
8}
9
10type PartialUser = Partial<User>;
11// { id?: string; name?: string; email?: string }
12
13// Required - make all properties required
14interface Config {
15 host?: string;
16 port?: number;
17}
18
19type RequiredConfig = Required<Config>;
20// { host: string; port: number }
21
22// Pick - select specific properties
23type UserName = Pick<User, 'name'>;
24// { name: string }
25
26// Omit - exclude specific properties
27type UserWithoutId = Omit<User, 'id'>;
28// { name: string; email: string }
29
30// Record - create object type with keys
31type Roles = 'admin' | 'user' | 'guest';
32type RolePermissions = Record<Roles, string[]>;
33// { admin: string[]; user: string[]; guest: string[] }Generic Functions with Objects#
1// Create object from entries
2function fromEntries<K extends string, V>(
3 entries: [K, V][]
4): Record<K, V> {
5 return Object.fromEntries(entries) as Record<K, V>;
6}
7
8const obj = fromEntries([
9 ['name', 'John'],
10 ['city', 'Boston']
11]);
12// { name: string; city: string }
13
14// Pick properties
15function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
16 const result = {} as Pick<T, K>;
17 keys.forEach(key => {
18 result[key] = obj[key];
19 });
20 return result;
21}
22
23const user = { id: 1, name: 'John', email: 'john@example.com' };
24const nameOnly = pick(user, ['name']);
25// { name: string }Generic React Components#
1// Generic list component
2interface ListProps<T> {
3 items: T[];
4 renderItem: (item: T) => React.ReactNode;
5}
6
7function List<T>({ items, renderItem }: ListProps<T>) {
8 return <ul>{items.map(renderItem)}</ul>;
9}
10
11// Usage
12<List
13 items={[{ id: 1, name: 'John' }]}
14 renderItem={user => <li key={user.id}>{user.name}</li>}
15/>
16
17// Generic select component
18interface SelectProps<T> {
19 options: T[];
20 value: T;
21 onChange: (value: T) => void;
22 getLabel: (option: T) => string;
23 getValue: (option: T) => string;
24}
25
26function Select<T>({ options, value, onChange, getLabel, getValue }: SelectProps<T>) {
27 return (
28 <select
29 value={getValue(value)}
30 onChange={e => {
31 const selected = options.find(o => getValue(o) === e.target.value);
32 if (selected) onChange(selected);
33 }}
34 >
35 {options.map(option => (
36 <option key={getValue(option)} value={getValue(option)}>
37 {getLabel(option)}
38 </option>
39 ))}
40 </select>
41 );
42}Common Patterns#
1// Factory function
2function createStore<T>(initial: T) {
3 let state = initial;
4
5 return {
6 get: () => state,
7 set: (value: T) => { state = value; },
8 update: (updater: (current: T) => T) => {
9 state = updater(state);
10 }
11 };
12}
13
14const numberStore = createStore(0);
15numberStore.set(5);
16numberStore.update(n => n + 1);
17
18// Builder pattern
19class QueryBuilder<T> {
20 private query: Partial<T> = {};
21
22 where<K extends keyof T>(key: K, value: T[K]): this {
23 this.query[key] = value;
24 return this;
25 }
26
27 build(): Partial<T> {
28 return { ...this.query };
29 }
30}
31
32interface User {
33 name: string;
34 age: number;
35 active: boolean;
36}
37
38const query = new QueryBuilder<User>()
39 .where('name', 'John')
40 .where('active', true)
41 .build();Best Practices#
Naming Conventions:
✓ T for single type parameter
✓ K for key type
✓ V for value type
✓ Descriptive names for complex cases
Constraints:
✓ Use extends for constraints
✓ Prefer specific constraints
✓ Document constraint requirements
✓ Use keyof for property access
Design:
✓ Start simple, add generics when needed
✓ Let TypeScript infer when possible
✓ Use defaults for common cases
✓ Keep type parameters minimal
Avoid:
✗ Unnecessary generics
✗ Too many type parameters
✗ Overly complex constraints
✗ Any as escape hatch
Conclusion#
Generics are fundamental to TypeScript for creating reusable, type-safe code. Start with simple type parameters and add constraints as needed. Use built-in utility types like Partial, Pick, and Record to transform types. Let TypeScript infer types when possible, and provide explicit type arguments only when necessary for clarity or disambiguation.