Back to Blog

Mastering TypeScript: Advanced Patterns

TypeScript's type system is incredibly powerful. Beyond basic type annotations, it offers a rich set of features that allow you to express complex constraints and relationships in your code.

Conditional Types

Conditional types allow you to create types that depend on other types:

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

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

Mapped Types

Mapped types let you create new types by transforming existing ones:

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

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

Template Literal Types

Template literal types enable string manipulation at the type level:

type EventName = `on${Capitalize<string>}`;
type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type Endpoint = `/${string}`;
type Route = `${HTTPMethod} ${Endpoint}`;

Practical Patterns

The Builder Pattern

class QueryBuilder<T extends Record<string, unknown>> {
  private conditions: string[] = [];

  where<K extends keyof T>(key: K, value: T[K]): this {
    this.conditions.push(`${String(key)} = ${value}`);
    return this;
  }

  build(): string {
    return `SELECT * WHERE ${this.conditions.join(' AND ')}`;
  }
}

Discriminated Unions

type Result<T, E> =
  | { success: true; data: T }
  | { success: false; error: E };

function handleResult(result: Result<string, Error>) {
  if (result.success) {
    console.log(result.data); // TypeScript knows data exists
  } else {
    console.error(result.error); // TypeScript knows error exists
  }
}

Key Takeaways

  1. Start simple - Don't over-engineer your types
  2. Use generics - They make your code reusable and type-safe
  3. Leverage inference - TypeScript can often figure out types automatically
  4. Use utility types - Partial, Required, Pick, Omit are your friends

Good types are documentation that never goes out of date.