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.

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
- Use strict mode - Enable strict type checking in tsconfig.json
- Avoid
any
- Useunknown
when you don't know the type - Leverage type inference - Don't over-annotate simple types
- Create reusable types - Use interfaces and type aliases
- 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.