Back to Articles
Advanced TypeScript Patterns with Visual Examples
Advanced TypeScript Patterns with Visual Examples

Explore advanced TypeScript patterns with syntax-highlighted code examples and visual diagrams demonstrating type relationships and architecture

Advanced TypeScript Patterns with Visual Examples

TypeScript's type system is incredibly powerful. Let's explore advanced patterns with real code examples and visual diagrams.

Conditional Types

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

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

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

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

function getUser() {
  return { id: 1, name: 'Alice', email: 'alice@example.com' };
}

type User = ReturnType<typeof getUser>;
// { id: number; name: string; email: string; }

Mapped Types

Transform existing types into new ones:

// Base interface
interface Person {
  name: string;
  age: number;
  email: string;
}

// Make all properties optional
type PartialPerson = Partial<Person>;

// Make all properties readonly
type ReadonlyPerson = Readonly<Person>;

// Pick specific properties
type PersonName = Pick<Person, 'name' | 'email'>;

// Omit specific properties
type PersonWithoutEmail = Omit<Person, 'email'>;

Generic Constraints

Use constraints to limit what types can be used:

interface HasLength {
  length: number;
}

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

logLength('hello'); // ✅ Works - string has length
logLength([1, 2, 3]); // ✅ Works - array has length
logLength({ length: 10 }); // ✅ Works - object has length
// logLength(123); // ❌ Error - number doesn't have length

Type System Architecture

Here's how TypeScript's type checking process works:

sequenceDiagram
    participant Dev as Developer
    participant TSC as TypeScript Compiler
    participant AST as Abstract Syntax Tree
    participant TC as Type Checker
    participant JS as JavaScript Output

    Dev->>TSC: Write TypeScript Code
    TSC->>AST: Parse Code
    AST->>TC: Analyze Types

    alt Type Error Found
        TC->>Dev: Report Type Error
    else Types Valid
        TC->>JS: Generate JavaScript
        JS->>Dev: Compiled Output
    end

Dependency Injection Pattern

Here's a typical DI container flow in TypeScript:

graph TD
    A[Application Start] --> B[Register Services]
    B --> C[Container]
    C --> D{Request Service}
    D -->|Singleton| E[Return Cached Instance]
    D -->|Transient| F[Create New Instance]
    E --> G[Inject Dependencies]
    F --> G
    G --> H[Return Service]

Utility Types Example

A practical example combining multiple patterns:

// API Response handler with utility types
interface ApiResponse<T> {
  data: T;
  error: string | null;
  loading: boolean;
}

// User entity
interface User {
  id: number;
  name: string;
  email: string;
  password: string;
  createdAt: Date;
}

// Public user (omit password)
type PublicUser = Omit<User, 'password'>;

// User creation payload (omit id and createdAt)
type CreateUserDto = Omit<User, 'id' | 'createdAt'>;

// User update payload (all fields optional except id)
type UpdateUserDto = Partial<Omit<User, 'id'>> & Pick<User, 'id'>;

// API handlers
async function getUser(id: number): Promise<ApiResponse<PublicUser>> {
  // Implementation
  return {
    data: {
      id: 1,
      name: 'Alice',
      email: 'alice@example.com',
      createdAt: new Date()
    },
    error: null,
    loading: false
  };
}

async function createUser(dto: CreateUserDto): Promise<ApiResponse<PublicUser>> {
  // Implementation
  return { data: {} as PublicUser, error: null, loading: false };
}

async function updateUser(dto: UpdateUserDto): Promise<ApiResponse<PublicUser>> {
  // Implementation
  return { data: {} as PublicUser, error: null, loading: false };
}

Builder Pattern with Fluent API

class QueryBuilder<T> {
  private conditions: string[] = [];
  private orderField?: string;
  private limitValue?: number;

  where(condition: string): this {
    this.conditions.push(condition);
    return this;
  }

  orderBy(field: keyof T): this {
    this.orderField = field as string;
    return this;
  }

  limit(count: number): this {
    this.limitValue = count;
    return this;
  }

  build(): string {
    let query = 'SELECT * FROM table';

    if (this.conditions.length > 0) {
      query += ` WHERE ${this.conditions.join(' AND ')}`;
    }

    if (this.orderField) {
      query += ` ORDER BY ${this.orderField}`;
    }

    if (this.limitValue) {
      query += ` LIMIT ${this.limitValue}`;
    }

    return query;
  }
}

// Usage
const query = new QueryBuilder<User>()
  .where('age > 18')
  .where('active = true')
  .orderBy('name')
  .limit(10)
  .build();

console.log(query);
// SELECT * FROM table WHERE age > 18 AND active = true ORDER BY name LIMIT 10

State Machine Diagram

Here's a user authentication state machine:

stateDiagram-v2
    [*] --> LoggedOut
    LoggedOut --> Authenticating: login()
    Authenticating --> LoggedIn: success
    Authenticating --> LoggedOut: failure
    LoggedIn --> RefreshingToken: token expired
    RefreshingToken --> LoggedIn: refresh success
    RefreshingToken --> LoggedOut: refresh failed
    LoggedIn --> LoggedOut: logout()
    LoggedIn --> [*]

Conclusion

These advanced TypeScript patterns enable you to build type-safe, maintainable applications. The combination of:

  • Conditional Types - Dynamic type logic
  • Mapped Types - Type transformations
  • Utility Types - Common patterns
  • Generic Constraints - Type boundaries
  • Visual Diagrams - Architecture clarity

Together, they form a powerful toolkit for modern TypeScript development.


Written with ❤️ using Claude Blog

Loading comments...