TypeScript Tips: Advanced Patterns for Better Code

Explore advanced TypeScript patterns and techniques that will make your code more type-safe, maintainable, and developer-friendly

ByYour Name
6 min read
TypeScriptJavaScriptProgrammingTypes

TypeScript Tips: Advanced Patterns for Better Code

TypeScript has become an essential tool in modern web development, but many developers only scratch the surface of its capabilities. Let's explore some advanced patterns that can significantly improve your code quality and development experience.

1. Conditional Types: Type-Level Logic

Conditional types allow you to create types that depend on other types, enabling powerful type-level programming:

type ApiResponse<T> = T extends string 
  ? { message: T; status: 'success' }
  : { data: T; status: 'success' } | { error: string; status: 'error' };

// Usage
type StringResponse = ApiResponse<string>; 
// { message: string; status: 'success' }

type UserResponse = ApiResponse<User>; 
// { data: User; status: 'success' } | { error: string; status: 'error' }

2. Template Literal Types: String Manipulation at Type Level

Template literal types let you manipulate strings at the type level:

type EventName<T extends string> = `on${Capitalize<T>}`;
type CssProperty<T extends string> = `--${T}`;

// Usage
type ClickEvent = EventName<'click'>; // 'onClick'
type ColorProperty = CssProperty<'primary-color'>; // '--primary-color'

// Building a theme system
type Theme = {
  [K in CssProperty<'primary' | 'secondary' | 'accent'>]: string;
};
// Results in: { '--primary': string; '--secondary': string; '--accent': string; }

3. Mapped Types: Transform Existing Types

Mapped types allow you to create new types by transforming properties of existing types:

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

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

// Custom example: Make specific fields optional
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

interface User {
  id: string;
  name: string;
  email: string;
  avatar?: string;
}

type CreateUserRequest = PartialBy<User, 'id' | 'avatar'>;
// { name: string; email: string; id?: string; avatar?: string; }

4. Utility Types for API Design

Create utility types that make your API more predictable and type-safe:

// Ensure all properties are functions
type Methods<T> = {
  [K in keyof T]: T[K] extends (...args: any[]) => any ? T[K] : never;
};

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

// Create a type-safe event emitter
interface EventMap {
  'user:login': { userId: string; timestamp: Date };
  'user:logout': { userId: string };
  'data:update': { data: any; source: string };
}

class TypedEventEmitter<T extends Record<string, any>> {
  emit<K extends keyof T>(event: K, data: T[K]): void {
    // Implementation
  }
  
  on<K extends keyof T>(event: K, handler: (data: T[K]) => void): void {
    // Implementation
  }
}

const emitter = new TypedEventEmitter<EventMap>();
emitter.emit('user:login', { userId: '123', timestamp: new Date() }); // ✅
emitter.emit('user:login', { userId: '123' }); // ❌ Missing timestamp

5. Branded Types: Prevent Primitive Obsession

Branded types help prevent accidentally mixing up values of the same primitive type:

type Brand<T, TBrand> = T & { __brand: TBrand };

type UserId = Brand<string, 'UserId'>;
type Email = Brand<string, 'Email'>;
type PostId = Brand<string, 'PostId'>;

function createUserId(id: string): UserId {
  return id as UserId;
}

function sendEmail(to: Email, subject: string): void {
  // Implementation
}

function getUser(id: UserId): User {
  // Implementation
}

const userId = createUserId('user-123');
const email = 'user@example.com' as Email;

getUser(userId); // ✅
getUser(email); // ❌ Type error
sendEmail(userId, 'Hello'); // ❌ Type error

6. Discriminated Unions: Type-Safe State Management

Use discriminated unions to model state that can be in one of several distinct states:

type AsyncState<T, E = Error> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: E };

function handleUserState(state: AsyncState<User>) {
  switch (state.status) {
    case 'idle':
      return <div>Click to load user</div>;
    case 'loading':
      return <div>Loading...</div>;
    case 'success':
      return <div>Hello, {state.data.name}!</div>; // TypeScript knows data exists
    case 'error':
      return <div>Error: {state.error.message}</div>; // TypeScript knows error exists
  }
}

7. Type Guards: Runtime Type Safety

Create custom type guards for runtime type checking:

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

function isUser(value: unknown): value is User {
  return (
    typeof value === 'object' &&
    value !== null &&
    'id' in value &&
    'name' in value &&
    'email' in value
  );
}

// Usage in API responses
async function fetchUser(id: string): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  const data = await response.json();
  
  if (!isUser(data)) {
    throw new Error('Invalid user data received');
  }
  
  return data; // TypeScript knows this is User
}

8. Generic Constraints: Flexible Yet Safe

Use generic constraints to create flexible but safe generic functions:

interface Identifiable {
  id: string;
}

function updateEntity<T extends Identifiable>(
  entities: T[],
  id: string,
  updates: Partial<Omit<T, 'id'>>
): T[] {
  return entities.map(entity =>
    entity.id === id 
      ? { ...entity, ...updates }
      : entity
  );
}

// Works with any type that has an id
const users = updateEntity(userList, 'user-1', { name: 'New Name' });
const posts = updateEntity(postList, 'post-1', { title: 'New Title' });

Conclusion

These advanced TypeScript patterns might seem complex at first, but they provide incredible value:

  • Better IDE support with more accurate autocomplete and error detection
  • Reduced runtime errors through compile-time checks
  • Self-documenting code where types serve as documentation
  • Improved refactoring with confidence that changes won't break contracts

Start incorporating these patterns gradually into your codebase. Your future self (and your teammates) will thank you!


Which TypeScript patterns have you found most useful in your projects? Share your favorite tips in the comments!