MeshWorld India LogoMeshWorld.

TypeScript Generics: Advanced Real-World Design Patterns

Listen to ArticleAI Speech
~11 min read narration
100%
TypeScript Generics: Advanced Real-World Design Patterns

TypeScript generics separate professional code from stringly-typed chaos. They let you write flexible, reusable components while keeping type safety. This guide covers the patterns you’ll actually use — not academic type gymnastics.

Basic Generics

Generic Functions

TYPESCRIPT
// Without generics — loses type information
function identity(arg: any): any {
    return arg;
}

// With generics — preserves type
function identity<T>(arg: T): T {
    return arg;
}

// Usage
const num = identity<number>(42);      // Type: number
const str = identity<string>('hello'); // Type: string
const inferred = identity([1, 2, 3]);  // Type: number[] (inferred)

Generic Interfaces

TYPESCRIPT
interface ApiResponse<T> {
    data: T;
    status: number;
    error?: string;
}

// Usage
interface User {
    id: string;
    name: string;
}

const userResponse: ApiResponse<User> = {
    data: { id: '1', name: 'Alice' },
    status: 200
};

const listResponse: ApiResponse<User[]> = {
    data: [{ id: '1', name: 'Alice' }],
    status: 200
};

Generic Classes

TYPESCRIPT
class Queue<T> {
    private items: T[] = [];
    
    enqueue(item: T): void {
        this.items.push(item);
    }
    
    dequeue(): T | undefined {
        return this.items.shift();
    }
    
    peek(): T | undefined {
        return this.items[0];
    }
    
    size(): number {
        return this.items.length;
    }
}

// Usage
const stringQueue = new Queue<string>();
stringQueue.enqueue('first');
stringQueue.enqueue('second');

const numberQueue = new Queue<number>();
numberQueue.enqueue(1);
numberQueue.enqueue(2);

Real-World Patterns

1. Repository Pattern with Generics

TYPESCRIPT
interface Entity {
    id: string;
    createdAt: Date;
    updatedAt: Date;
}

class Repository<T extends Entity> {
    private items: Map<string, T> = new Map();
    
    create(item: Omit<T, 'id' | 'createdAt' | 'updatedAt'>): T {
        const now = new Date();
        const newItem = {
            ...item,
            id: crypto.randomUUID(),
            createdAt: now,
            updatedAt: now
        } as T;
        
        this.items.set(newItem.id, newItem);
        return newItem;
    }
    
    findById(id: string): T | undefined {
        return this.items.get(id);
    }
    
    findAll(): T[] {
        return Array.from(this.items.values());
    }
    
    update(id: string, updates: Partial<Omit<T, 'id' | 'createdAt'>>): T | undefined {
        const existing = this.items.get(id);
        if (!existing) return undefined;
        
        const updated = {
            ...existing,
            ...updates,
            updatedAt: new Date()
        };
        
        this.items.set(id, updated);
        return updated;
    }
    
    delete(id: string): boolean {
        return this.items.delete(id);
    }
}

// Usage
interface Product extends Entity {
    name: string;
    price: number;
    inStock: boolean;
}

const productRepo = new Repository<Product>();

const product = productRepo.create({
    name: 'Laptop',
    price: 999,
    inStock: true
});

const updated = productRepo.update(product.id, { price: 899 });

2. API Client with Type Safety

TYPESCRIPT
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';

interface RequestConfig<TBody = unknown> {
    method: HttpMethod;
    url: string;
    body?: TBody;
    headers?: Record<string, string>;
}

interface ApiResponse<T> {
    data: T;
    status: number;
    headers: Headers;
}

class ApiClient {
    private baseUrl: string;
    
    constructor(baseUrl: string) {
        this.baseUrl = baseUrl;
    }
    
    async request<TResponse, TBody = unknown>(
        config: RequestConfig<TBody>
    ): Promise<ApiResponse<TResponse>> {
        const response = await fetch(`${this.baseUrl}${config.url}`, {
            method: config.method,
            headers: {
                'Content-Type': 'application/json',
                ...config.headers
            },
            body: config.body ? JSON.stringify(config.body) : undefined
        });
        
        const data = await response.json();
        
        return {
            data,
            status: response.status,
            headers: response.headers
        };
    }
    
    // Convenience methods
    get<T>(url: string) {
        return this.request<T>({ method: 'GET', url });
    }
    
    post<T, B>(url: string, body: B) {
        return this.request<T, B>({ method: 'POST', url, body });
    }
    
    put<T, B>(url: string, body: B) {
        return this.request<T, B>({ method: 'PUT', url, body });
    }
    
    patch<T, B>(url: string, body: B) {
        return this.request<T, B>({ method: 'PATCH', url, body });
    }
    
    delete<T>(url: string) {
        return this.request<T>({ method: 'DELETE', url });
    }
}

// Usage
interface User {
    id: string;
    email: string;
    name: string;
}

interface CreateUserDto {
    email: string;
    name: string;
}

const api = new ApiClient('https://api.example.com');

// Fully typed API calls
const { data: users } = await api.get<User[]>('/users');
const { data: newUser } = await api.post<User, CreateUserDto>('/users', {
    email: 'alice@example.com',
    name: 'Alice'
});
const { data: updated } = await api.patch<User, Partial<User>>(
    `/users/${newUser.id}`,
    { name: 'Alice Smith' }
);

3. Event Emitter with Type Safety

TYPESCRIPT
type EventMap = Record<string, any>;

type EventKey<T extends EventMap> = string & keyof T;
type EventHandler<T> = (payload: T) => void;

class TypedEventEmitter<Events extends EventMap> {
    private handlers: {
        [K in keyof Events]?: EventHandler<Events[K]>[]
    } = {};
    
    on<K extends EventKey<Events>>(
        event: K,
        handler: EventHandler<Events[K]>
    ): () => void {
        if (!this.handlers[event]) {
            this.handlers[event] = [];
        }
        this.handlers[event]!.push(handler);
        
        // Return unsubscribe function
        return () => this.off(event, handler);
    }
    
    off<K extends EventKey<Events>>(
        event: K,
        handler: EventHandler<Events[K]>
    ): void {
        const handlers = this.handlers[event];
        if (handlers) {
            const index = handlers.indexOf(handler);
            if (index > -1) {
                handlers.splice(index, 1);
            }
        }
    }
    
    emit<K extends EventKey<Events>>(event: K, payload: Events[K]): void {
        const handlers = this.handlers[event];
        if (handlers) {
            handlers.forEach(h => h(payload));
        }
    }
}

// Usage
interface MyEvents {
    'user:login': { userId: string; timestamp: Date };
    'user:logout': { userId: string };
    'data:update': { table: string; records: number };
    'error': { message: string; code: number };
}

const emitter = new TypedEventEmitter<MyEvents>();

// Type-safe event handling
emitter.on('user:login', ({ userId, timestamp }) => {
    console.log(`User ${userId} logged in at ${timestamp}`);
});

emitter.on('data:update', ({ table, records }) => {
    console.log(`${table} updated with ${records} records`);
});

// Type error: wrong payload shape
// emitter.emit('user:login', { userId: '1' }); // Error: missing timestamp

// Correct usage
emitter.emit('user:login', {
    userId: 'user-123',
    timestamp: new Date()
});

Advanced Patterns

4. Constrained Generics

TYPESCRIPT
// Only accept types with an 'id' property
interface HasId {
    id: string;
}

function findById<T extends HasId>(items: T[], id: string): T | undefined {
    return items.find(item => item.id === id);
}

// Multiple constraints
interface HasTimestamps {
    createdAt: Date;
    updatedAt: Date;
}

function sortByDate<T extends HasTimestamps>(items: T[]): T[] {
    return [...items].sort((a, b) => 
        b.createdAt.getTime() - a.createdAt.getTime()
    );
}

// Keyof constraints
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
    return obj[key];
}

const user = { id: '1', name: 'Alice', age: 30 };
const name = getProperty(user, 'name'); // Type: string
const age = getProperty(user, 'age');   // Type: number
// const wrong = getProperty(user, 'email'); // Error: 'email' doesn't exist

5. Conditional Types

TYPESCRIPT
// Basic conditional type
type IsString<T> = T extends string ? true : false;

type A = IsString<'hello'>;  // true
type B = IsString<123>;      // false

// Extract type from array
type ElementType<T> = T extends (infer E)[] ? E : T;

type Numbers = ElementType<number[]>;  // number
type StringOrNum = ElementType<string>; // string

// Flatten nested arrays
type Flatten<T> = T extends (infer E)[] ? Flatten<E> : T;

type DeepArray = Flatten<string[][][]>; // string

// Create nullable version
type Nullable<T> = T | null;

type NullableString = Nullable<string>; // string | null

// Non-null type
type NonNullable<T> = T extends null | undefined ? never : T;

type DefinitelyString = NonNullable<string | null>; // string

6. Mapped Types

TYPESCRIPT
// Make all properties optional
type Partial<T> = {
    [P in keyof T]?: T[P];
};

// Make all properties required
type Required<T> = {
    [P in keyof T]-?: T[P];
};

// Make all properties readonly
type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};

// Remove readonly
type Mutable<T> = {
    -readonly [P in keyof T]: T[P];
};

// Pick specific properties
type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};

// Omit specific properties
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;

// Record type
type Record<K extends keyof any, T> = {
    [P in K]: T;
};

// Usage examples
interface User {
    id: string;
    email: string;
    name: string;
    password: string;
    createdAt: Date;
}

// API response doesn't include password
type UserResponse = Omit<User, 'password'>;

// Form input only needs name and email
type UserFormInput = Pick<User, 'name' | 'email'>;

// Update can have partial fields
type UserUpdate = Partial<Omit<User, 'id' | 'createdAt'>>;

// Readonly for immutable state
type ImmutableUser = Readonly<User>;

7. Template Literal Types

TYPESCRIPT
// Create event names from actions
type EventName<T extends string> = `on${Capitalize<T>}`;

type ClickEvent = EventName<'click'>; // 'onClick'
type HoverEvent = EventName<'hover'>; // 'onHover'

// CSS property types
type CSSProperty = 'margin' | 'padding' | 'border';
type CSSDirection = 'top' | 'right' | 'bottom' | 'left';

type CSSPropertyWithDirection = `${CSSProperty}${Capitalize<CSSDirection>}`;
// 'marginTop' | 'marginRight' | 'marginBottom' | 'marginLeft'
// 'paddingTop' | 'paddingRight' | ... etc

// Route parameters
type Route<Path extends string> = 
    Path extends `${infer Start}/:${infer Param}/${infer Rest}`
        ? { [K in Param]: string } & Route<`${Start}/${Rest}`>
        : Path extends `${string}/:${infer Param}`
            ? { [K in Param]: string }
            : {};

// Usage
 type UserRoute = Route<'/users/:userId/posts/:postId'>;
// { userId: string; postId: string }

8. Infer with Generics

TYPESCRIPT
// Extract return type
type ReturnType<T extends (...args: any[]) => any> = 
    T extends (...args: any[]) => infer R ? R : never;

function createUser() {
    return { id: '1', name: 'Alice' };
}

type User = ReturnType<typeof createUser>;
// { id: string; name: string }

// Extract parameters
type Parameters<T extends (...args: any[]) => any> =
    T extends (...args: infer P) => any ? P : never;

function updateUser(id: string, data: Partial<User>) {
    return { ...data, id };
}

type UpdateUserParams = Parameters<typeof updateUser>;
// [string, Partial<User>]

// Extract Promise type
type Awaited<T> = T extends Promise<infer U> ? U : T;

async function fetchUser(): Promise<User> {
    return { id: '1', name: 'Alice' };
}

type FetchedUser = Awaited<ReturnType<typeof fetchUser>>;
// User (not Promise<User>)

Utility Types Reference

TypePurposeExample
Partial<T>All properties optionalPartial<User>
Required<T>All properties requiredRequired<Config>
Readonly<T>All properties readonlyReadonly<State>
Pick<T, K>Select specific keysPick<User, 'id' | 'name'>
Omit<T, K>Remove specific keysOmit<User, 'password'>
Record<K, V>Dictionary typeRecord<string, User>
Exclude<T, U>Remove types from unionExclude<'a' | 'b', 'a'>
Extract<T, U>Extract types from unionExtract<'a' | 'b', 'a'>
NonNullable<T>Remove null/undefinedNonNullable<string | null>
ReturnType<T>Function return typeReturnType<typeof fn>
Parameters<T>Function parametersParameters<typeof fn>
Awaited<T>Unwrap PromiseAwaited<Promise<User>>

Common Patterns

Factory Pattern

TYPESCRIPT
interface EntityConstructor<T extends HasId> {
    new (data: Omit<T, 'id'>): T;
}

function createEntity<T extends HasId>(
    Constructor: EntityConstructor<T>,
    data: Omit<T, 'id'>
): T {
    return new Constructor(data);
}

class Product implements HasId {
    id: string;
    name: string;
    price: number;
    
    constructor(data: Omit<Product, 'id'>) {
        this.id = crypto.randomUUID();
        this.name = data.name;
        this.price = data.price;
    }
}

const product = createEntity(Product, { name: 'Laptop', price: 999 });

Builder Pattern

TYPESCRIPT
class QueryBuilder<T extends Record<string, any>> {
    private filters: Partial<T> = {};
    private sortKey: keyof T | null = null;
    private sortDirection: 'asc' | 'desc' = 'asc';
    
    where<K extends keyof T>(key: K, value: T[K]): this {
        this.filters[key] = value;
        return this;
    }
    
    orderBy(key: keyof T, direction: 'asc' | 'desc' = 'asc'): this {
        this.sortKey = key;
        this.sortDirection = direction;
        return this;
    }
    
    build(): { filters: Partial<T>; sort: { key: keyof T; direction: 'asc' | 'desc' } | null } {
        return {
            filters: this.filters,
            sort: this.sortKey ? { key: this.sortKey, direction: this.sortDirection } : null
        };
    }
}

interface User {
    id: string;
    name: string;
    age: number;
    active: boolean;
}

const query = new QueryBuilder<User>()
    .where('active', true)
    .where('age', 25)
    .orderBy('name', 'asc')
    .build();

Summary

  • Generics add flexibility while preserving type safety
  • Constraints (extends) ensure types have required properties
  • Conditional types create type-level logic
  • Mapped types transform object shapes
  • Utility types solve common transformation needs
  • Template literals create type-safe string patterns

Master these patterns and you’ll write TypeScript that’s both flexible and bulletproof.


Reader Quality Feedback

Did this technical guide help solve your problem?

Suggest Errata ($0)
Scarlett
Primary Author

Scarlett

Frontend Engineer and Design Systems specialist. Focused on building type-safe UI architectures, scalable CSS patterns, and accessible user experiences that bridge the gap between design and logic.

Explore Author Archive
Compute Fuel & Open Testbed
100% Independent & Verified

Fuel High-Density, Zero-Fluff Engineering Deep-Dives

Every guide on MeshWorld is validated on physical Linux nodes and reproducible testbeds. If this article saved you hours of debugging or unblocked production, consider funding our next cluster run.

Weekly Dispatch

Join MeshWorld Dispatch

Get practical tutorials, system blueprints, and curated AI engineering notes straight to your inbox. No fluff, zero spam.

Zero spam. 1-click unsubscribe anytime.Prefer RSS?
Curated Continuations

Up Next in This Domain.

Browse Full Archive