TypeScript Generics
/ 4 min read
Understanding Generics
Generics allow you to write flexible, reusable code that works with multiple types while maintaining type safety.
Basic Generic Syntax
1. Generic Functions
// Simple generic functionfunction identity<T>(arg: T): T { return arg;}
// Usageconst numberResult = identity<number>(42);const stringResult = identity("Hello"); // Type inference
// Multiple type parametersfunction pair<T, U>(first: T, second: U): [T, U] { return [first, second];}2. Generic Interfaces
// Generic interfaceinterface Box<T> { value: T; getValue(): T;}
// Implementationclass NumberBox implements Box<number> { constructor(public value: number) {}
getValue(): number { return this.value; }}Generic Constraints
1. Basic Constraints
// Constraint using extendsinterface Lengthwise { length: number;}
function logLength<T extends Lengthwise>(arg: T): number { return arg.length;}
// UsagelogLength("Hello"); // Works with stringlogLength([1, 2, 3]); // Works with arraylogLength({ length: 5, value: 10 }); // Works with object2. Multiple Constraints
interface HasName { name: string;}
interface HasAge { age: number;}
function printNameAndAge<T extends HasName & HasAge>(obj: T): void { console.log(`${obj.name} is ${obj.age} years old`);}Generic Classes
1. Basic Generic Class
class Container<T> { private item: T;
constructor(item: T) { this.item = item; }
getItem(): T { return this.item; }
setItem(item: T): void { this.item = item; }}
// Usageconst numberContainer = new Container<number>(123);const stringContainer = new Container("Hello");2. Generic Class with Constraints
class DataStorage<T extends string | number | boolean> { private data: T[] = [];
addItem(item: T) { this.data.push(item); }
removeItem(item: T) { const index = this.data.indexOf(item); if (index !== -1) { this.data.splice(index, 1); } }
getItems(): T[] { return [...this.data]; }}Advanced Generic Patterns
1. Generic Type Aliases
// Generic type aliastype Pair<T, U> = { first: T; second: U;};
// Generic function typetype Operation<T> = (a: T, b: T) => T;
// Usageconst numberPair: Pair<number, string> = { first: 42, second: "Hello"};
const add: Operation<number> = (a, b) => a + b;2. Generic Mapped Types
// Make all properties optionaltype Partial<T> = { [P in keyof T]?: T[P];};
// Make all properties readonlytype Readonly<T> = { readonly [P in keyof T]: T[P];};
// Make all properties nullabletype Nullable<T> = { [P in keyof T]: T[P] | null;};Generic Utility Types
1. Built-in Utility Types
// Record typetype PageInfo = Record<string, string>;
// Pick typeinterface User { id: number; name: string; email: string;}
type UserBasicInfo = Pick<User, "name" | "email">;
// Omit typetype UserWithoutId = Omit<User, "id">;2. Custom Utility Types
// DeepPartial typetype DeepPartial<T> = { [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];};
// NonNullable propertiestype NonNullableProps<T> = { [P in keyof T]: NonNullable<T[P]>;};Generic Conditional Types
1. Basic Conditional Types
type TypeName<T> = T extends string ? "string" : T extends number ? "number" : T extends boolean ? "boolean" : T extends undefined ? "undefined" : T extends Function ? "function" : "object";
// Usagetype T0 = TypeName<string>; // "string"type T1 = TypeName<number>; // "number"2. Infer Keyword
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
type ArrayElementType<T> = T extends (infer U)[] ? U : never;
// Usagetype Func = () => number;type FuncReturn = ReturnType<Func>; // number
type NumberArray = number[];type Element = ArrayElementType<NumberArray>; // numberGeneric Patterns in Practice
1. Factory Pattern
interface Product { name: string; price: number;}
class GenericFactory<T extends Product> { create(name: string, price: number): T { return { name, price } as T; }}
// Usageinterface Book extends Product { author: string;}
const bookFactory = new GenericFactory<Book>();2. Repository Pattern
interface Repository<T> { find(id: number): Promise<T>; findAll(): Promise<T[]>; create(item: T): Promise<T>; update(id: number, item: T): Promise<T>; delete(id: number): Promise<void>;}
class GenericRepository<T> implements Repository<T> { constructor(private items: T[] = []) {}
async find(id: number): Promise<T> { return this.items[id]; }
async findAll(): Promise<T[]> { return [...this.items]; }
async create(item: T): Promise<T> { this.items.push(item); return item; }
async update(id: number, item: T): Promise<T> { this.items[id] = item; return item; }
async delete(id: number): Promise<void> { this.items.splice(id, 1); }}Best Practices
- Use meaningful type parameter names
- Apply constraints when necessary
- Avoid over-generalization
- Use type inference when possible
- Document generic parameters
- Consider performance implications
- Test with different type arguments
Common Pitfalls
- Over-constraining generics
- Not constraining enough
- Using any instead of proper generics
- Forgetting type inference capabilities
- Not considering edge cases
Conclusion
Generics are a powerful feature in TypeScript that enable you to write flexible, reusable, and type-safe code. Understanding and properly implementing generics is crucial for building robust TypeScript applications.
Series Navigation
- Previous: TypeScript Interfaces and Classes
- Next: TypeScript Advanced Types