Back to Blog
TypeScriptJavaScriptProgrammingBest Practices

Mastering TypeScript: Advanced Patterns for Better Code

Dive deep into advanced TypeScript patterns and techniques that will help you write more maintainable, type-safe code in your projects.

D
Dheeraj Jha
August 22, 2024
4 min read
Mastering TypeScript: Advanced Patterns for Better Code

Mastering TypeScript: Advanced Patterns for Better Code

TypeScript has revolutionized how we write JavaScript by adding static typing and advanced features. In this post, we'll explore advanced patterns that will elevate your TypeScript skills.

Understanding Type Inference

TypeScript's type inference is powerful, but understanding when and how to use it effectively is crucial:

// Let TypeScript infer simple types
const name = "Dheeraj" // string
const age = 25 // number

// Be explicit with complex types
interface User {
  id: string
  name: string
  email: string
}

const user: User = {
  id: "123",
  name: "Dheeraj",
  email: "[email protected]"
}

Generics: Writing Reusable Code

Generics allow you to write flexible, reusable functions and classes:

// Generic function
function first<T>(arr: T[]): T | undefined {
  return arr[0]
}

const numbers = [1, 2, 3]
const firstNumber = first(numbers) // number | undefined

const strings = ["a", "b", "c"]
const firstString = first(strings) // string | undefined

Advanced Generic Patterns

// Conditional types with generics
type NonNullable<T> = T extends null | undefined ? never : T

type Result = NonNullable<string | null> // string

// Generic constraints
interface HasId {
  id: string
}

function findById<T extends HasId>(items: T[], id: string): T | undefined {
  return items.find(item => item.id === id)
}

Utility Types for Common Patterns

TypeScript provides built-in utility types that solve common problems:

Partial and Required

interface Todo {
  title: string
  description: string
  completed: boolean
}

// Make all properties optional
type PartialTodo = Partial<Todo>

// Make all properties required
type RequiredTodo = Required<PartialTodo>

Pick and Omit

// Pick specific properties
type TodoPreview = Pick<Todo, 'title' | 'completed'>

// Omit specific properties
type TodoWithoutDescription = Omit<Todo, 'description'>

Record for Object Types

type UserRoles = 'admin' | 'user' | 'guest'

type RolePermissions = Record<UserRoles, string[]>

const permissions: RolePermissions = {
  admin: ['read', 'write', 'delete'],
  user: ['read', 'write'],
  guest: ['read']
}

Type Guards for Runtime Safety

Type guards help you narrow types at runtime:

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

function processValue(value: string | number) {
  if (isString(value)) {
    // TypeScript knows value is string here
    console.log(value.toUpperCase())
  } else {
    // TypeScript knows value is number here
    console.log(value.toFixed(2))
  }
}

Discriminated Unions

Create type-safe state machines with discriminated unions:

type LoadingState = 
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: any }
  | { status: 'error'; error: Error }

function handleState(state: LoadingState) {
  switch (state.status) {
    case 'idle':
      return 'Not started'
    case 'loading':
      return 'Loading...'
    case 'success':
      return `Data: ${state.data}`
    case 'error':
      return `Error: ${state.error.message}`
  }
}

Template Literal Types

Create powerful string manipulation types:

type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'
type Endpoint = '/users' | '/posts' | '/comments'

type APIRoute = `${HTTPMethod} ${Endpoint}`

// Valid routes:
const route1: APIRoute = 'GET /users'
const route2: APIRoute = 'POST /posts'

Best Practices

  1. Use strict mode - Enable strict type checking in tsconfig.json
  2. Avoid any - Use unknown when you don't know the type
  3. Leverage type inference - Don't over-annotate simple types
  4. Create reusable types - Use interfaces and type aliases
  5. Document complex types - Add comments for clarity

Conclusion

Mastering these advanced TypeScript patterns will help you write more robust, maintainable code. The type system is your ally in catching bugs before they reach production.

Keep practicing these patterns, and you'll find your code becomes more self-documenting and easier to refactor over time.