Advanced TypeScript Patterns for Better Code Quality
Quick Answer
Advanced TypeScript patterns like conditional types, mapped types, and discriminated unions help you write more type-safe code. These patterns enable better IDE support, catch errors at compile-time, and make refactoring safer.
Why Advanced TypeScript?
TypeScript's type system is incredibly powerful, but many developers only scratch the surface. Let's explore patterns that will take your TypeScript skills to the next level.
Conditional Types
Conditional types allow you to create types that depend on other types:
// Basic conditional type
type IsString<T> = T extends string ? true : false
type A = IsString<string> // true
type B = IsString<number> // false
// Practical example: Extract return type
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never
function getUser() {
return { id: 1, name: 'John' }
}
type User = ReturnType<typeof getUser> // { id: number; name: string }
Distributive Conditional Types
Conditional types distribute over union types:
type ToArray<T> = T extends any ? T[] : never
type StrOrNum = string | number
type StrOrNumArray = ToArray<StrOrNum> // string[] | number[]
Mapped Types
Transform properties of existing types:
// Make all properties optional
type Partial<T> = {
[P in keyof T]?: T[P]
}
// Make all properties readonly
type Readonly<T> = {
readonly [P in keyof T]: T[P]
}
// Pick specific properties
type Pick<T, K extends keyof T> = {
[P in K]: T[P]
}
interface User {
id: number
name: string
email: string
age: number
}
type UserPreview = Pick<User, 'id' | 'name'>
// { id: number; name: string }
Advanced Mapped Types
// Deep partial - makes nested objects optional too
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]
}
// Remove readonly modifiers
type Mutable<T> = {
-readonly [P in keyof T]: T[P]
}
// Required - opposite of Partial
type Required<T> = {
[P in keyof T]-?: T[P]
}
Discriminated Unions
Type-safe state management:
type LoadingState = {
status: 'loading'
}
type SuccessState<T> = {
status: 'success'
data: T
}
type ErrorState = {
status: 'error'
error: Error
}
type AsyncState<T> = LoadingState | SuccessState<T> | ErrorState
function handleState<T>(state: AsyncState<T>) {
switch (state.status) {
case 'loading':
// TypeScript knows: state = LoadingState
return 'Loading...'
case 'success':
// TypeScript knows: state = SuccessState<T>
return state.data // ✓ data exists
case 'error':
// TypeScript knows: state = ErrorState
return state.error.message // ✓ error exists
}
}
Type Guards
Custom runtime type checking:
// User-defined type guard
interface Dog {
bark: () => void
}
interface Cat {
meow: () => void
}
function isDog(animal: Dog | Cat): animal is Dog {
return (animal as Dog).bark !== undefined
}
function makeSound(animal: Dog | Cat) {
if (isDog(animal)) {
animal.bark() // TypeScript knows it's a Dog
} else {
animal.meow() // TypeScript knows it's a Cat
}
}
Advanced Type Guards
// Generic type guard
function isArrayOf<T>(
arr: unknown,
check: (item: unknown) => item is T
): arr is T[] {
return Array.isArray(arr) && arr.every(check)
}
function isString(value: unknown): value is string {
return typeof value === 'string'
}
const data: unknown = ['a', 'b', 'c']
if (isArrayOf(data, isString)) {
// TypeScript knows: data is string[]
data.forEach(str => str.toUpperCase())
}
Template Literal Types
Create sophisticated string types:
// Event system with type safety
type EventName = 'click' | 'focus' | 'blur'
type HandlerName = `on${Capitalize<EventName>}` // 'onClick' | 'onFocus' | 'onBlur'
type EventHandlers = {
[K in HandlerName]: () => void
}
// API routes
type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'
type Resource = 'users' | 'posts' | 'comments'
type Endpoint = `/${Resource}` | `/${Resource}/:id`
// Route handlers
type RouteHandler = {
[K in `${HTTPMethod} ${Endpoint}`]: (req: Request) => Response
}
Utility Types in Action
Combine multiple patterns:
// Deep readonly
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object
? DeepReadonly<T[P]>
: T[P]
}
// Non-nullable
type NonNullable<T> = T extends null | undefined ? never : T
// Extract function parameters
type Parameters<T extends (...args: any) => any> =
T extends (...args: infer P) => any ? P : never
function greet(name: string, age: number) {
return `Hello ${name}, you are ${age}`
}
type GreetParams = Parameters<typeof greet> // [string, number]
Branded Types
Create nominal types for additional safety:
// Brand pattern
type Brand<K, T> = K & { __brand: T }
type USD = Brand<number, 'USD'>
type EUR = Brand<number, 'EUR'>
const usd = 100 as USD
const eur = 100 as EUR
// This will error - can't mix currencies!
// const total: USD = usd + eur
// Helper function
function usd(amount: number): USD {
return amount as USD
}
function eur(amount: number): EUR {
return amount as EUR
}
Builder Pattern with Types
Fluent interfaces with type safety:
class QueryBuilder<T, Selected extends keyof T = never> {
private selectFields: Set<keyof T> = new Set()
select<K extends keyof T>(
...fields: K[]
): QueryBuilder<T, Selected | K> {
fields.forEach(f => this.selectFields.add(f))
return this as any
}
build(): Pick<T, Selected> {
// Implementation
return {} as Pick<T, Selected>
}
}
interface User {
id: number
name: string
email: string
age: number
}
const query = new QueryBuilder<User>()
.select('id', 'name')
.build()
// Type is: { id: number; name: string }
Best Practices
1. Prefer Type Inference
// ✗ Avoid
const numbers: number[] = [1, 2, 3]
// ✓ Better
const numbers = [1, 2, 3] // Type inferred as number[]
2. Use unknown Instead of any
// ✗ Avoid
function process(data: any) {
return data.value
}
// ✓ Better
function process(data: unknown) {
if (typeof data === 'object' && data !== null && 'value' in data) {
return (data as { value: unknown }).value
}
throw new Error('Invalid data')
}
3. Leverage satisfies Operator
type Route = { path: string; handler: () => void }
const routes = {
home: { path: '/', handler: () => {} },
about: { path: '/about', handler: () => {} },
} satisfies Record<string, Route>
// Can still access specific keys
routes.home.path // ✓ Works, type: string
Conclusion
Advanced TypeScript patterns enable you to:
- Catch more errors at compile-time
- Improve IDE autocomplete and refactoring
- Make impossible states unrepresentable
- Create more maintainable code
Start incorporating these patterns gradually, and your TypeScript code will become more robust and maintainable.
Further Reading
Complete SaaS Development Checklist
Download our comprehensive 50-point checklist to build your SaaS product right the first time.
Download Free ChecklistFrootsyTech Solutions
Expert Software Development Team
Enterprise Software Development, Cloud Architecture, Full-Stack Engineering
FrootsyTech Solutions is an agile, expert-led software development agency specializing in web and mobile applications. Our team brings decades of combined experience in building scalable, production-ready solutions for businesses worldwide.
Related Articles
Getting Started with Next.js 15: A Comprehensive Guide
Learn how to build modern web applications with Next.js 15, the React framework that makes building full-stack applications easier than ever.
Generating PDFs, Excel, and PowerPoint Reports in Next.js
Complete guide to implementing multi-format report exports in Next.js applications. Learn to generate PDFs with charts, Excel workbooks with formatting, and PowerPoint presentations using client and server-side rendering.
Building Multi-Step Form Wizards with React Hook Form and Zod
Learn how to build production-ready multi-step form wizards with React Hook Form and Zod validation. Includes step-by-step validation, progress tracking, data persistence, and file uploads with accessibility in mind.