Denis Kirevby Denis Kirev

TypeScript Best Practices and Advanced Patterns

Learn advanced TypeScript patterns, best practices, and optimization techniques.

4 min read
TYPESCRIPTJAVASCRIPTGUIDE

Type System Fundamentals

1. Use Strict Mode

Always enable strict mode in your tsconfig.json:

{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true
  }
}

2. Leverage Type Inference

TypeScript's type inference is powerful. Use it when types are obvious:

// Good - type is inferred
const numbers = [1, 2, 3]
const user = {
  name: 'John',
  age: 30,
}

// Avoid - unnecessary type annotation
const numbers: number[] = [1, 2, 3]

Advanced Types

1. Discriminated Unions

Use discriminated unions for type-safe handling of different states:

type Success = {
  status: 'success'
  data: User[]
}

type Error = {
  status: 'error'
  message: string
}

type ApiResponse = Success | Error

function handleResponse(response: ApiResponse) {
  if (response.status === 'success') {
    return response.data // TypeScript knows it's User[]
  }
  return response.message // TypeScript knows it's string
}

2. Utility Types

Make use of built-in utility types:

interface User {
  id: number
  name: string
  email: string
  password: string
}

// Only pick necessary fields
type UserDTO = Pick<User, 'id' | 'name' | 'email'>

// Make all fields optional
type PartialUser = Partial<User>

// Make all fields readonly
type ReadonlyUser = Readonly<User>

Performance Optimization

1. Type Imports

Use type imports to avoid runtime overhead:

// Good - only imports type information
import type { User } from './types'

// Avoid - imports entire module
import { User } from './types'

2. Const Assertions

Use const assertions for better type inference and performance:

const config = {
  api: {
    url: 'https://api.example.com',
    version: 'v1',
  },
} as const

// Now config.api.url is type "https://api.example.com" instead of string

Error Handling

1. Custom Error Types

Create specific error types for better error handling:

class ValidationError extends Error {
  constructor(
    message: string,
    public field: string
  ) {
    super(message)
    this.name = 'ValidationError'
  }
}

function validateUser(user: User) {
  if (!user.email.includes('@')) {
    throw new ValidationError('Invalid email format', 'email')
  }
}

2. Result Types

Use Result types for explicit error handling:

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

function divide(a: number, b: number): Result<number> {
  if (b === 0) {
    return { success: false, error: new Error('Division by zero') }
  }
  return { success: true, value: a / b }
}

Generic Patterns

1. Factory Pattern

Use generics with factory patterns:

interface Repository<T> {
  find(id: string): Promise<T>
  save(item: T): Promise<void>
}

class GenericRepository<T> implements Repository<T> {
  constructor(private collection: string) {}

  async find(id: string): Promise<T> {
    // Implementation
  }

  async save(item: T): Promise<void> {
    // Implementation
  }
}

// Usage
const userRepo = new GenericRepository<User>('users')

2. Builder Pattern

Implement type-safe builders:

class QueryBuilder<T> {
  private filters: Partial<T> = {}

  where<K extends keyof T>(key: K, value: T[K]): this {
    this.filters[key] = value
    return this
  }

  build(): Partial<T> {
    return this.filters
  }
}

// Usage
const query = new QueryBuilder<User>().where('age', 30).where('name', 'John').build()

Testing

1. Type Testing

Use expectType and expectError for type testing:

import { expectType, expectError } from 'tsd'

expectType<string>('hello')
expectError<number>('world')

2. Mock Types

Create proper type definitions for mocks:

type Mock<T> = {
  [P in keyof T]: T[P] extends (...args: any[]) => any
    ? jest.Mock<ReturnType<T[P]>, Parameters<T[P]>>
    : T[P]
}

const userServiceMock: Mock<UserService> = {
  getUser: jest.fn(),
  updateUser: jest.fn(),
}

Code Organization

1. Barrel Exports

Use barrel exports for cleaner imports:

// types/index.ts
export * from './user'
export * from './auth'
export * from './api'

// Usage
import { User, Auth, Api } from './types'

2. Module Augmentation

Extend existing types safely:

declare module 'express-session' {
  interface SessionData {
    userId: string
    isAdmin: boolean
  }
}

Conclusion

Following these TypeScript best practices will help you write more maintainable, type-safe, and performant code. Remember to:

  • Enable strict mode
  • Use type inference wisely
  • Leverage utility types
  • Implement proper error handling
  • Use generics for reusable code
  • Write type tests
  • Organize your code effectively

Keep your TypeScript skills sharp by staying updated with the latest features and patterns in the ecosystem.

Last updated: January 9, 2025