Skip to main content
โšก Calmops

TypeScript Advanced Patterns: Beyond the Basics

Introduction

TypeScript’s type system is remarkably powerful. Beyond basic types, advanced features enable sophisticated patterns for type-safe APIs, runtime validation, and code generation. This guide explores advanced TypeScript patterns that will elevate your code.

Generics Deep Dive

Generic Constraints

// Basic generic
function first<T>(arr: T[]): T | undefined {
  return arr[0];
}

// Constraining to a type
interface HasLength {
  length: number;
}

function logLength<T extends HasLength>(item: T): void {
  console.log(item.length);
}

// Now works with string, array, but not number
logLength("hello");     // OK
logLength([1, 2, 3]);  // OK
logLength({ length: 5 }); // OK
// logLength(123);     // Error: number has no length

Generic Classes

interface Repository<T> {
  find(id: string): Promise<T | null>;
  save(entity: T): Promise<T>;
  delete(id: string): Promise<void>;
}

class UserRepository implements Repository<User> {
  private users: Map<string, User> = new Map();
  
  async find(id: string): Promise<User | null> {
    return this.users.get(id) ?? null;
  }
  
  async save(user: User): Promise<User> {
    this.users.set(user.id, user);
    return user;
  }
  
  async delete(id: string): Promise<void> {
    this.users.delete(id);
  }
}

Generic Factories

type Constructor<T> = new (...args: any[]) => T;

function createService<T>(
  base: Constructor<T>,
  apiClient: ApiClient
): T & { api: ApiClient } {
  const instance = new base();
  return Object.assign(instance, { api: apiClient });
}

class UserService {
  async getUser(id: string) { /* ... */ }
}

const service = createService(UserService, new ApiClient());
service.getUser('123');    // From UserService
service.api.get('/users');  // From apiClient

Conditional Types

Basic Conditionals

type IsString<T> = T extends string ? true : false;

type A = IsString<string>;  // true
type B = IsString<number>;  // false

Extract and Exclude

type T = string | number | boolean;

// Extract strings
type Strings = Extract<T, string>;  // string

// Exclude strings
type NonStrings = Exclude<T, string>;  // number | boolean

Practical Example: API Response Types

type APISuccess<T> = {
  success: true;
  data: T;
};

type APIError = {
  success: false;
  error: {
    code: string;
    message: string;
  };
};

type APIResponse<T> = APISuccess<T> | APIError;

// Type-safe response handler
function handleResponse<T>(response: APIResponse<T>) {
  if (response.success) {
    // TypeScript knows response.data exists here
    return response.data;
  }
  // TypeScript knows error exists here
  throw new Error(response.error.message);
}

Template Literal Types

Basic Usage

type EventName = 'click' | 'focus' | 'blur';
type Handler = `on${Capitalize<EventName>}`;
// Handler = "onClick" | "onFocus" | "onBlur"

type HTTPMethod = 'get' | 'post' | 'put' | 'delete';
type Endpoint = `/${string}`;

type Route = `${Uppercase<HTTPMethod>} ${Endpoint}`;
// Route = "GET /users" | "POST /users" | ...

Building Type-Safe APIs

type PathParams<T extends string> = 
  T extends `${string}:${infer P}/${infer Rest}` 
    ? P | PathParams<`/${Rest}`>
  : T extends `${string}:${infer P}` 
    ? P 
    : never;

type Route = '/users/:id/posts/:postId';
type Params = PathParams<Route>;  // "id" | "postId"

// Extract params into an object
type RouteParams<T extends string> = 
  T extends `${string}:${infer}`
    ? { [K in P P}/${infer Rest]: string } & RouteParams<`/${Rest}`>
    : T extends `${string}:${infer P}`
    ? { [K in P]: string }
    : {};

type Params = RouteParams<Route>;
// { id: string; postId: string }

Mapped Types

Basic Mapping

type Readonly<T> = {
  readonly [K in keyof T]: T[K];
};

type Partial<T> = {
  [K in keyof T]?: T[K];
};

type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

Practical Mapped Types

// Make all properties optional recursively
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

type User = {
  name: string;
  address: {
    street: string;
    city: string;
  };
};

type PartialUser = DeepPartial<User>;
// { name?: string; address?: { street?: string; city?: string } }

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

Utility Types

Custom Utility Types

// Omit multiple keys
type OmitKeys<T, K extends keyof T> = {
  [P in Exclude<keyof T, K>]: T[P];
};

// Pick optional keys
type PickOptional<T> = {
  [P in keyof T as T[P] extends undefined ? never : P]: T[P];
};

// Make specific keys nullable
type Nullable<T, K extends keyof T> = Omit<T, K> & {
  [P in K]: T[P] | null;
};

// Example usage
interface User {
  id: string;
  name: string;
  email: string;
  createdAt: Date;
}

type CreateUser = OmitKeys<User, 'id' | 'createdAt'>;
type UserWithEmail = Nullable<User, 'email'>;

Type Guards

Custom Type Guards

interface Fish {
  swim(): void;
}

interface Bird {
  fly(): void;
}

function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined;
}

function move(pet: Fish | Bird) {
  if (isFish(pet)) {
    pet.swim();  // TypeScript knows it's Fish
  } else {
    pet.fly();   // TypeScript knows it's Bird
  }
}

Discriminated Unions

type Action =
  | { type: 'increment'; amount: number }
  | { type: 'decrement'; amount: number }
  | { type: 'reset' };

function reducer(action: Action) {
  switch (action.type) {
    case 'increment':
      return action.amount;  // TypeScript knows amount exists
    case 'decrement':
      return action.amount;
    case 'reset':
      return 0;
  }
}

Declaration Merging

Interface Merging

interface User {
  name: string;
}

interface User {
  age: number;
}

// Merged: User { name: string; age: number; }

Namespace Merging

namespace Validation {
  export function isEmail(value: string): boolean {
    return value.includes('@');
  }
}

namespace Validation {
  export function isUrl(value: string): boolean {
    return value.startsWith('http');
  }
}

// Now Validation has both functions

Decorators (Experimental)

Class Decorators

function singleton<T extends Constructor>(constructor: T) {
  let instance: InstanceType<T> | null = null;
  
  return class extends constructor {
    constructor(...args: any[]) {
      if (!instance) {
        super(...args);
        instance = this as InstanceType<T>;
      }
      return instance;
    }
  };
}

@singleton
class Database {
  constructor() {}
}

const db1 = new Database();
const db2 = new Database();
console.log(db1 === db2);  // true

Method Decorators

function logged<T>(
  target: any,
  propertyKey: string,
  descriptor: TypedPropertyDescriptor<T>
) {
  const original = descriptor.value!;
  
  descriptor.value = function (...args: any[]) {
    console.log(`Calling ${propertyKey} with:`, args);
    const result = original.apply(this, args);
    console.log(`Result:`, result);
    return result;
  } as T;
  
  return descriptor;
}

class Calculator {
  @logged
  add(a: number, b: number): number {
    return a + b;
  }
}

Type-Safe Event System

type EventMap = {
  click: { x: number; y: number };
  keypress: { key: string };
  focus: void;
};

class EventEmitter<T extends Record<string, any>> {
  private listeners: {
    [K in keyof T]?: Array<(payload: T[K]) => void>;
  } = {};
  
  on<K extends keyof T>(event: K, callback: (payload: T[K]) => void) {
    this.listeners[event]?.push(callback);
  }
  
  emit<K extends keyof T>(event: K, payload: T[K]) {
    this.listeners[event]?.forEach(cb => cb(payload));
  }
}

const emitter = new EventEmitter<EventMap>();
emitter.on('click', ({ x, y }) => console.log(x, y));
emitter.emit('click', { x: 10, y: 20 });

Conclusion

TypeScript’s advanced types enable powerful patterns for building type-safe applications. From generics to conditional types, mapped types to decorators, these tools help catch errors at compile time and create self-documenting code. Master these patterns to level up your TypeScript skills.


Resources

Comments