What are Generics?
Generics allow you to create reusable components that work with a variety of types while maintaining type safety.
Generic Functions
Basic Generic Function
typescript
function identity<T>(arg: T): T {
return arg;
}
const num = identity<number>(42); // 42
const str = identity('hello'); // Type inferenceMultiple Type Parameters
typescript
function pair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
const p1 = pair(1, 'one'); // [number, string]
const p2 = pair('hello', 42); // [string, number]Generic Constraints
typescript
// Constrain to specific properties
function logLength<T extends { length: number }>(arg: T): void {
console.log(arg.length);
}
logLength('hello'); // OK - string has length
logLength([1, 2, 3]); // OK - array has length
logLength(42); // Error - number doesn't have length
// Constrain to specific type
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: 'John', age: 30 };
const name = getProperty(user, 'name'); // string
const age = getProperty(user, 'age'); // number
const invalid = getProperty(user, 'email'); // ErrorDefault Type Parameters
typescript
function createArray<T = string>(length: number, value: T): T[] {
return Array(length).fill(value);
}
const strings = createArray(3, 'hello'); // string[]
const numbers = createArray<number>(3, 42); // number[]
const defaults = createArray(3, true); // boolean[]Generic Classes
Basic Generic Class
typescript
class Box<T> {
private contents: T;
constructor(value: T) {
this.contents = value;
}
get(): T {
return this.contents;
}
set(value: T): void {
this.contents = value;
}
}
const stringBox = new Box('hello');
const numBox = new Box(42);Generic Class with Constraints
typescript
class NumberWrapper<T extends number> {
constructor(public value: T) {}
add(n: T): T {
return (this.value + n) as T;
}
}
const intWrapper = new NumberWrapper(10);
const floatWrapper = new NumberWrapper(3.14);Generic Class with Multiple Types
typescript
class Pair<T, U> {
constructor(
public first: T,
public second: U
) {}
swap(): Pair<U, T> {
return new Pair(this.second, this.first);
}
}
const pair = new Pair(1, 'one');
const swapped = pair.swap(); // Pair<string, number>Generic Interfaces
Basic Generic Interface
typescript
interface Repository<T> {
findById(id: string): Promise<T | null>;
findAll(): Promise<T[]>;
create(entity: Omit<T, 'id'>): Promise<T>;
update(id: string, updates: Partial<T>): Promise<T>;
delete(id: string): Promise<void>;
}
interface User {
id: string;
name: string;
email: string;
}
class UserRepository implements Repository<User> {
async findById(id: string): Promise<User | null> {
// Implementation
return null;
}
// ... other methods
}Generic Interface with Constraints
typescript
interface Identifiable {
id: string | number;
}
interface DataService<T extends Identifiable> {
get(id: T['id']): Promise<T>;
save(entity: T): Promise<void>;
}
class UserService implements DataService<User> {
async get(id: string): Promise<User> {
// Implementation
return {} as User;
}
async save(user: User): Promise<void> {
// Implementation
}
}Advanced Generic Patterns
Conditional Types
typescript
type NonNullable<T> = T extends null | undefined ? never : T;
type Flatten<T> = T extends any[] ? T[number] : T;
type Nested = number[][];
type Flat = Flatten<Nested>; // number[]Mapped Types with Generics
typescript
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
type Nullable<T> = {
[P in keyof T]: T[P] | null;
};
type Partial<T> = {
[P in keyof T]?: T[P];
};Utility Functions
typescript
// Generic mapper
function map<T, U>(array: T[], fn: (item: T) => U): U[] {
return array.map(fn);
}
const numbers = [1, 2, 3];
const doubled = map(numbers, n => n * 2); // number[]
const strings = map(numbers, n => n.toString()); // string[]
// Generic filter
function filter<T>(array: T[], predicate: (item: T) => boolean): T[] {
return array.filter(predicate);
}
const evens = filter(numbers, n => n % 2 === 0); // number[]Generic Pipeline
typescript
type Pipeline<T> = T extends []
? never
: T extends [infer First]
? First
: T extends [infer First, ...infer Rest]
? First extends (arg: any) => any
? Pipeline<Rest> extends never
? never
: (arg: ReturnType<First>) => ReturnType<First>
: never
: never;
// Usage
const add = (n: number) => n + 1;
const double = (n: number) => n * 2;
const toString = (n: number) => n.toString();
type Result = Pipeline<[typeof add, typeof double, typeof toString]>;
// (arg: string) => stringPractical Examples
API Client
typescript
class ApiClient {
async get<T>(url: string): Promise<T> {
const response = await fetch(url);
return response.json();
}
async post<T, U>(url: string, data: T): Promise<U> {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
return response.json();
}
}
interface User {
id: number;
name: string;
}
interface Post {
id: number;
title: string;
}
const client = new ApiClient();
const user = await client.get<User>('/api/users/1');
const created = await client.post<User, Post>('/api/posts', {
title: 'Hello',
});State Manager
typescript
class Store<T> {
private state: T;
private listeners: Array<(state: T) => void> = [];
constructor(initialState: T) {
this.state = initialState;
}
getState(): T {
return this.state;
}
setState(partial: Partial<T>): void {
this.state = { ...this.state, ...partial };
this.notify();
}
subscribe(listener: (state: T) => void): () => void {
this.listeners.push(listener);
return () => {
this.listeners = this.listeners.filter(l => l !== listener);
};
}
private notify(): void {
this.listeners.forEach(listener => listener(this.state));
}
}
interface AppState {
user: User | null;
loading: boolean;
}
const store = new Store<AppState>({
user: null,
loading: false,
});
store.subscribe((state) => {
console.log('State changed:', state);
});Generic Repository
typescript
abstract class BaseRepository<T extends { id: number }> {
constructor(protected db: any) {}
async findById(id: number): Promise<T | null> {
const result = await this.db.query(
'SELECT * FROM table WHERE id = ?',
[id]
);
return result[0] || null;
}
async findAll(): Promise<T[]> {
return this.db.query('SELECT * FROM table');
}
async create(entity: Omit<T, 'id'>): Promise<T> {
const result = await this.db.insert(entity);
return { ...entity, id: result.insertId };
}
async update(id: number, updates: Partial<T>): Promise<T> {
await this.db.update(id, updates);
return this.findById(id) as Promise<T>;
}
async delete(id: number): Promise<void> {
await this.db.delete(id);
}
}
class UserRepository extends BaseRepository<User> {
async findByEmail(email: string): Promise<User | null> {
const result = await this.db.query(
'SELECT * FROM users WHERE email = ?',
[email]
);
return result[0] || null;
}
}Best Practices
- 1Use descriptive type names: T, U, V for simple cases
- 2Constrain generics: Use extends for better type safety
- 3Provide defaults: For common use cases
- 4Document generics: Explain type parameters
- 5Prefer inference: Let TypeScript infer when possible
- 6Avoid overuse: Keep code readable
Conclusion
Generics enable flexible, reusable code while maintaining type safety. Master generics to write better TypeScript code.