Advanced TypeScript Patterns: Beyond the Basics

TypeScript’s type system is incredibly powerful once you move beyond basic types. Let’s explore advanced patterns that will make your code more robust, maintainable, and expressive.

Conditional Types and Type Guards

Advanced Type Guards

// Type predicate functions
function isString(value: unknown): value is string {
  return typeof value === 'string';
}

function isNumber(value: unknown): value is number {
  return typeof value === 'number' && !isNaN(value);
}

// Generic type guard
function isArrayOf<T>(
  value: unknown,
  guard: (item: unknown) => item is T
): value is T[] {
  return Array.isArray(value) && value.every(guard);
}

// Usage
const data: unknown = ['hello', 'world'];
if (isArrayOf(data, isString)) {
  // data is now typed as string[]
  data.forEach(str => console.log(str.toUpperCase()));
}

// Discriminated unions with type guards
type ApiResponse<T> = 
  | { success: true; data: T }
  | { success: false; error: string };

function isSuccessResponse<T>(
  response: ApiResponse<T>
): response is { success: true; data: T } {
  return response.success === true;
}

// Usage
async function handleApiCall<T>(response: ApiResponse<T>) {
  if (isSuccessResponse(response)) {
    // response.data is available and typed
    return response.data;
  } else {
    // response.error is available
    throw new Error(response.error);
  }
}

Conditional Types

// Basic conditional type
type IsArray<T> = T extends any[] ? true : false;

type Test1 = IsArray<string[]>; // true
type Test2 = IsArray<string>;   // false

// Conditional type with infer
type ArrayElement<T> = T extends (infer U)[] ? U : never;

type StringElement = ArrayElement<string[]>; // string
type NumberElement = ArrayElement<number[]>; // number

// Advanced conditional types
type NonNullable<T> = T extends null | undefined ? never : T;

type FunctionReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type AsyncReturnType<T> = T extends (...args: any[]) => Promise<infer R> ? R : never;

// Recursive conditional types
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};

type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

Mapped Types and Template Literals

Advanced Mapped Types

// Create getters and setters
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

type Setters<T> = {
  [K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void;
};

interface User {
  name: string;
  age: number;
  email: string;
}

type UserGetters = Getters<User>;
// {
//   getName: () => string;
//   getAge: () => number;
//   getEmail: () => string;
// }

type UserSetters = Setters<User>;
// {
//   setName: (value: string) => void;
//   setAge: (value: number) => void;
//   setEmail: (value: string) => void;
// }

// Filter properties by type
type PickByType<T, U> = {
  [K in keyof T as T[K] extends U ? K : never]: T[K];
};

type OmitByType<T, U> = {
  [K in keyof T as T[K] extends U ? never : K]: T[K];
};

interface Mixed {
  id: number;
  name: string;
  isActive: boolean;
  tags: string[];
  createdAt: Date;
}

type StringProps = PickByType<Mixed, string>; // { name: string }
type NonStringProps = OmitByType<Mixed, string>; 
// { id: number; isActive: boolean; tags: string[]; createdAt: Date }

Template Literal Types

// API endpoint types
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type ApiVersion = 'v1' | 'v2';
type Resource = 'users' | 'posts' | 'comments';

type ApiEndpoint = `/${ApiVersion}/${Resource}`;
type ApiRoute = `${HttpMethod} ${ApiEndpoint}`;

// Event system with template literals
type EventMap = {
  'user:created': { userId: string; email: string };
  'user:updated': { userId: string; changes: Partial<User> };
  'user:deleted': { userId: string };
  'post:published': { postId: string; authorId: string };
};

type EventName = keyof EventMap;
type EventHandler<T extends EventName> = (data: EventMap[T]) => void;

class EventEmitter {
  private handlers: {
    [K in EventName]?: EventHandler<K>[];
  } = {};

  on<T extends EventName>(event: T, handler: EventHandler<T>) {
    if (!this.handlers[event]) {
      this.handlers[event] = [];
    }
    this.handlers[event]!.push(handler);
  }

  emit<T extends EventName>(event: T, data: EventMap[T]) {
    const eventHandlers = this.handlers[event];
    if (eventHandlers) {
      eventHandlers.forEach(handler => handler(data));
    }
  }
}

// Usage
const emitter = new EventEmitter();
emitter.on('user:created', (data) => {
  // data is typed as { userId: string; email: string }
  console.log(`User created: ${data.email}`);
});

Utility Types and Transformations

Custom Utility Types

// Make specific properties optional
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

// Make specific properties required
type RequiredBy<T, K extends keyof T> = T & Required<Pick<T, K>>;

// Rename keys
type RenameKeys<T, M extends Record<keyof T, string>> = {
  [K in keyof M]: T[K extends keyof T ? K : never];
};

interface OriginalUser {
  firstName: string;
  lastName: string;
  emailAddress: string;
}

type RenamedUser = RenameKeys<OriginalUser, {
  firstName: 'name';
  lastName: 'surname';
  emailAddress: 'email';
}>;
// { name: string; surname: string; email: string }

// Deep key paths
type DeepKeyOf<T> = {
  [K in keyof T]: T[K] extends object 
    ? K | `${K & string}.${DeepKeyOf<T[K]> & string}`
    : K;
}[keyof T];

interface NestedObject {
  user: {
    profile: {
      name: string;
      settings: {
        theme: string;
      };
    };
  };
  posts: Array<{ title: string }>;
}

type Paths = DeepKeyOf<NestedObject>;
// "user" | "posts" | "user.profile" | "user.profile.name" | "user.profile.settings" | "user.profile.settings.theme"

Function Type Utilities

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

// Create overloaded function types
type Overload<T> = T extends {
  (...args: infer A1): infer R1;
  (...args: infer A2): infer R2;
} ? {
  (...args: A1): R1;
  (...args: A2): R2;
} : never;

// Curry function type
type Curry<T> = T extends (...args: infer A) => infer R
  ? A extends [infer H, ...infer T]
    ? (arg: H) => Curry<(...args: T) => R>
    : R
  : never;

// Example curry implementation
function curry<T extends (...args: any[]) => any>(fn: T): Curry<T> {
  return ((...args: any[]) => {
    if (args.length >= fn.length) {
      return fn(...args);
    }
    return curry(fn.bind(null, ...args));
  }) as Curry<T>;
}

// Usage
const add = (a: number, b: number, c: number) => a + b + c;
const curriedAdd = curry(add);
const result = curriedAdd(1)(2)(3); // 6

Advanced Generic Patterns

Generic Constraints and Inference

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

// Multiple generic constraints
interface Identifiable {
  id: string;
}

interface Timestamped {
  createdAt: Date;
  updatedAt: Date;
}

function updateEntity<T extends Identifiable & Timestamped>(
  entity: T,
  updates: Partial<Omit<T, 'id' | 'createdAt'>>
): T {
  return {
    ...entity,
    ...updates,
    updatedAt: new Date()
  };
}

// Generic factories
interface Repository<T> {
  findById(id: string): Promise<T | null>;
  create(data: Omit<T, 'id'>): Promise<T>;
  update(id: string, data: Partial<T>): Promise<T>;
  delete(id: string): Promise<void>;
}

class BaseRepository<T extends Identifiable> implements Repository<T> {
  constructor(private model: new () => T) {}

  async findById(id: string): Promise<T | null> {
    // Implementation
    return null;
  }

  async create(data: Omit<T, 'id'>): Promise<T> {
    // Implementation
    return new this.model();
  }

  async update(id: string, data: Partial<T>): Promise<T> {
    // Implementation
    return new this.model();
  }

  async delete(id: string): Promise<void> {
    // Implementation
  }
}

// Usage
interface User extends Identifiable {
  name: string;
  email: string;
}

class UserModel implements User {
  id!: string;
  name!: string;
  email!: string;
}

const userRepo = new BaseRepository(UserModel);

Type-Safe API Clients

Building Type-Safe HTTP Clients

// API schema definition
interface ApiSchema {
  '/users': {
    GET: {
      response: User[];
      query?: { page?: number; limit?: number };
    };
    POST: {
      body: Omit<User, 'id'>;
      response: User;
    };
  };
  '/users/:id': {
    GET: {
      params: { id: string };
      response: User;
    };
    PUT: {
      params: { id: string };
      body: Partial<User>;
      response: User;
    };
    DELETE: {
      params: { id: string };
      response: void;
    };
  };
}

// Extract types from schema
type ExtractParams<T> = T extends { params: infer P } ? P : never;
type ExtractQuery<T> = T extends { query: infer Q } ? Q : never;
type ExtractBody<T> = T extends { body: infer B } ? B : never;
type ExtractResponse<T> = T extends { response: infer R } ? R : never;

// Type-safe API client
class ApiClient {
  constructor(private baseUrl: string) {}

  async request<
    Path extends keyof ApiSchema,
    Method extends keyof ApiSchema[Path]
  >(
    path: Path,
    method: Method,
    options?: {
      params?: ExtractParams<ApiSchema[Path][Method]>;
      query?: ExtractQuery<ApiSchema[Path][Method]>;
      body?: ExtractBody<ApiSchema[Path][Method]>;
    }
  ): Promise<ExtractResponse<ApiSchema[Path][Method]>> {
    // Build URL with params
    let url = `${this.baseUrl}${path}`;
    if (options?.params) {
      Object.entries(options.params).forEach(([key, value]) => {
        url = url.replace(`:${key}`, String(value));
      });
    }

    // Add query parameters
    if (options?.query) {
      const searchParams = new URLSearchParams();
      Object.entries(options.query).forEach(([key, value]) => {
        if (value !== undefined) {
          searchParams.append(key, String(value));
        }
      });
      url += `?${searchParams.toString()}`;
    }

    const response = await fetch(url, {
      method: method as string,
      headers: {
        'Content-Type': 'application/json',
      },
      body: options?.body ? JSON.stringify(options.body) : undefined,
    });

    return response.json();
  }
}

// Usage - fully type-safe!
const api = new ApiClient('https://api.example.com');

// TypeScript knows the exact types for each call
const users = await api.request('/users', 'GET', {
  query: { page: 1, limit: 10 }
}); // users: User[]

const user = await api.request('/users/:id', 'GET', {
  params: { id: '123' }
}); // user: User

const newUser = await api.request('/users', 'POST', {
  body: { name: 'John', email: 'john@example.com' }
}); // newUser: User

Advanced TypeScript patterns unlock incredible type safety and developer experience. These patterns help catch errors at compile time, provide better IntelliSense, and make your code self-documenting. Start with the basics and gradually incorporate these advanced patterns as your TypeScript skills grow.