Generics: Functions, Classes, and Constraints in TypeScript
Generics enable writing reusable, type-safe code. This article covers generic functions, classes, constraints, and advanced patterns.
Introduction
Generics provide:
- Code reusability
- Type safety
- Flexibility
- Maintainability
- Better IDE support
Understanding generics helps you:
- Write reusable code
- Create flexible APIs
- Maintain type safety
- Build scalable systems
- Reduce code duplication
Generic Functions
Basic Generic Functions
// โ
Good: Basic generic function
function identity<T>(value: T): T {
return value;
}
const str = identity<string>('hello');
const num = identity<number>(42);
const bool = identity<boolean>(true);
// โ
Good: Type inference
const str2 = identity('hello'); // T inferred as string
const num2 = identity(42); // T inferred as number
// โ
Good: Multiple type parameters
function pair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
const result = pair<string, number>('hello', 42);
const result2 = pair('hello', 42); // Types inferred
Generic Constraints
// โ
Good: Constrain generic types
function getLength<T extends { length: number }>(value: T): number {
return value.length;
}
getLength('hello'); // OK
getLength([1, 2, 3]); // OK
getLength({ length: 5 }); // OK
// getLength(42); // Error: number has no length
// โ
Good: Extend specific type
function getProperty<T extends object, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: 'John', age: 30 };
const name = getProperty(user, 'name'); // OK
// getProperty(user, 'email'); // Error: 'email' not in user
// โ
Good: Conditional constraints
function process<T extends string | number>(value: T): T {
return value;
}
process('hello'); // OK
process(42); // OK
// process(true); // Error
Generic Utility Functions
// โ
Good: Generic array functions
function first<T>(array: T[]): T | undefined {
return array[0];
}
function last<T>(array: T[]): T | undefined {
return array[array.length - 1];
}
function reverse<T>(array: T[]): T[] {
return array.reverse();
}
// โ
Good: Generic filter
function filter<T>(array: T[], predicate: (item: T) => boolean): T[] {
return array.filter(predicate);
}
const numbers = [1, 2, 3, 4, 5];
const evens = filter(numbers, (n) => n % 2 === 0);
// โ
Good: Generic map
function map<T, U>(array: T[], transform: (item: T) => U): U[] {
return array.map(transform);
}
const lengths = map(['hello', 'world'], (s) => s.length);
Generic Classes
Basic Generic Classes
// โ
Good: Generic class
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<string>('hello');
const numberContainer = new Container<number>(42);
// โ
Good: Generic with constraints
class Repository<T extends { id: number }> {
private items: T[] = [];
add(item: T): void {
this.items.push(item);
}
getById(id: number): T | undefined {
return this.items.find((item) => item.id === id);
}
getAll(): T[] {
return this.items;
}
}
interface User {
id: number;
name: string;
}
const userRepo = new Repository<User>();
userRepo.add({ id: 1, name: 'John' });
Generic Inheritance
// โ
Good: Generic class inheritance
class BaseRepository<T extends { id: number }> {
protected items: T[] = [];
add(item: T): void {
this.items.push(item);
}
getById(id: number): T | undefined {
return this.items.find((item) => item.id === id);
}
}
class UserRepository extends BaseRepository<User> {
findByName(name: string): User | undefined {
return this.items.find((user) => user.name === name);
}
}
// โ
Good: Multiple type parameters
class Pair<T, U> {
constructor(private first: T, private second: U) {}
getFirst(): T {
return this.first;
}
getSecond(): U {
return this.second;
}
}
const pair = new Pair<string, number>('hello', 42);
Advanced Generic Patterns
Generic Constraints with Keyof
// โ
Good: Use keyof for type-safe property access
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'); // OK
// getProperty(user, 'email'); // Error
// โ
Good: Generic object mapper
function mapObject<T, K extends keyof T, U>(
obj: T,
key: K,
transform: (value: T[K]) => U
): U {
return transform(obj[key]);
}
const result = mapObject(user, 'name', (name) => name.toUpperCase());
Conditional Generic Types
// โ
Good: Conditional types with generics
type Flatten<T> = T extends Array<infer U> ? U : T;
type Str = Flatten<string[]>; // string
type Num = Flatten<number>; // number
// โ
Good: Extract type from Promise
type Awaited<T> = T extends Promise<infer U> ? U : T;
type PromiseString = Awaited<Promise<string>>; // string
type PlainNumber = Awaited<number>; // number
// โ
Good: Generic with conditional
function process<T>(value: T): T extends string ? number : string {
if (typeof value === 'string') {
return value.length as any;
}
return 'not a string' as any;
}
Generic Decorators
// โ
Good: Generic decorator
function memoize<T extends (...args: any[]) => any>(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
const cache = new Map();
descriptor.value = function (...args: any[]) {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = originalMethod.apply(this, args);
cache.set(key, result);
return result;
};
return descriptor;
}
class Calculator {
@memoize
fibonacci(n: number): number {
if (n <= 1) return n;
return this.fibonacci(n - 1) + this.fibonacci(n - 2);
}
}
Practical Examples
Generic API Client
// โ
Good: Generic API client
interface ApiResponse<T> {
status: number;
data: T;
error?: string;
}
class ApiClient {
async get<T>(url: string): Promise<ApiResponse<T>> {
const response = await fetch(url);
const data = await response.json();
return {
status: response.status,
data
};
}
async post<T>(url: string, body: any): Promise<ApiResponse<T>> {
const response = await fetch(url, {
method: 'POST',
body: JSON.stringify(body)
});
const data = await response.json();
return {
status: response.status,
data
};
}
}
interface User {
id: number;
name: string;
}
const client = new ApiClient();
const response = await client.get<User>('/api/users/1');
Generic State Management
// โ
Good: Generic state store
class Store<T> {
private state: T;
private listeners: ((state: T) => void)[] = [];
constructor(initialState: T) {
this.state = initialState;
}
getState(): T {
return this.state;
}
setState(newState: T): void {
this.state = newState;
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;
isLoading: boolean;
}
const store = new Store<AppState>({
user: null,
isLoading: false
});
store.subscribe((state) => {
console.log('State updated:', state);
});
Best Practices
-
Use meaningful type parameter names:
// โ Good function process<T, U>(first: T, second: U): [T, U] { } // โ Bad function process<A, B>(first: A, second: B): [A, B] { } -
Constrain when possible:
// โ Good function getLength<T extends { length: number }>(value: T): number { return value.length; } // โ Bad function getLength<T>(value: T): number { return (value as any).length; } -
Use keyof for type safety:
// โ Good function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key]; } // โ Bad function getProperty<T>(obj: T, key: string): any { return (obj as any)[key]; }
Common Mistakes
-
Over-generalizing:
// โ Bad function process<T>(value: T): T { return value; // Too generic } // โ Good function process<T extends string | number>(value: T): T { return value; } -
Not using constraints:
// โ Bad function getLength<T>(value: T): number { return (value as any).length; } // โ Good function getLength<T extends { length: number }>(value: T): number { return value.length; }
Summary
Generics are powerful. Key takeaways:
- Use generics for reusable code
- Constrain types appropriately
- Use keyof for type safety
- Leverage type inference
- Use conditional types
- Apply to functions and classes
- Maintain type safety
- Document generic parameters
Related Resources
Next Steps
- Learn about Decorators and Metadata
- Explore Advanced Type Patterns
- Study TypeScript Best Practices
- Practice generic patterns
- Build reusable libraries
Comments