Skip to content
Published on

Advanced TypeScript Patterns Mastery: From Generics to Type Gymnastics — 2025 Practical Guide

Authors

Introduction

TypeScript has become the standard of the JavaScript ecosystem. With rapid evolution from TypeScript 5.4 through 5.7 during 2024-2025, the expressiveness of the type system has become even more powerful. Modern TypeScript development goes beyond simply adding types — it's about implementing logic at the type level and ensuring runtime safety.

This guide systematically covers advanced TypeScript patterns from generics basics through Conditional Types, Mapped Types, Template Literal Types, Branded Types, Zod + tRPC integration, and Type Challenges. Each section includes practical code examples you can apply immediately.


1. TypeScript 5.x New Features (2024-2025)

TypeScript 5.4 — NoInfer and groupBy

The most notable feature in TypeScript 5.4 is the NoInfer utility type. It controls whether a specific parameter participates in type inference for generic functions.

// Without NoInfer — defaultValue influences T inference
function getOrDefault<T>(value: T | undefined, defaultValue: T): T {
  return value ?? defaultValue
}

// With NoInfer — defaultValue doesn't participate in T inference
function getOrDefault<T>(value: T | undefined, defaultValue: NoInfer<T>): T {
  return value ?? defaultValue
}

// Usage
const result = getOrDefault('hello', 42)
// Without NoInfer: T = string | number
// With NoInfer: T = string, error on 42

Object.groupBy and Map.groupBy are now officially supported:

const users = [
  { name: 'Alice', role: 'admin' },
  { name: 'Bob', role: 'user' },
  { name: 'Charlie', role: 'admin' },
]

const grouped = Object.groupBy(users, (user) => user.role)
// Type: Partial<Record<string, User[]>>

TypeScript 5.5 — Inferred Type Predicates

TypeScript 5.5 allows the compiler to automatically infer type narrowing without explicit type guard declarations.

// Before: explicit type guard required
function isString(value: unknown): value is string {
  return typeof value === 'string'
}

// TS 5.5: automatic inference
const values = [1, 'hello', null, 'world', 3]
const strings = values.filter((v) => typeof v === 'string')
// In TS 5.5, strings is string[] (previously (string | number | null)[])

The isolatedDeclarations option enables delegating .d.ts generation to external tools (SWC, oxc, etc.):

{
  "compilerOptions": {
    "isolatedDeclarations": true,
    "declaration": true
  }
}

TypeScript 5.6 — Iterator Helpers

Support for the ECMAScript Iterator Helpers proposal enables chaining-style iterator manipulation:

function* fibonacci(): Generator<number> {
  let a = 0,
    b = 1
  while (true) {
    yield a
    ;[a, b] = [b, a + b]
  }
}

// Iterator helpers (TS 5.6)
const first10Even = fibonacci()
  .filter((n) => n % 2 === 0)
  .take(10)
  .toArray()

The --noUncheckedSideEffectImports flag verifies the existence of side-effect imports:

// Error if this file doesn't exist
import './polyfills.js'
import 'some-module/register'

TypeScript 5.7 — ES2024 Target and Path Rewriting

{
  "compilerOptions": {
    "target": "es2024",
    "rewriteRelativeImportExtensions": true
  }
}

The rewriteRelativeImportExtensions option automatically converts .ts imports to .js in output:

// Source code
import { helper } from './utils.ts'

// Compiled output
import { helper } from './utils.js'

2. Generics Master Class

Basic Generics

Generics are a core TypeScript feature that creates reusable code by accepting types as parameters.

// The most basic generic function
function identity<T>(arg: T): T {
  return arg
}

// Generic interface
interface Box<T> {
  value: T
  map<U>(fn: (value: T) => U): Box<U>
}

function createBox<T>(value: T): Box<T> {
  return {
    value,
    map: (fn) => createBox(fn(value)),
  }
}

const numberBox = createBox(42) // Box<number>
const stringBox = numberBox.map(String) // Box<string>

Constraints (extends)

// Basic constraint
function getLength<T extends { length: number }>(arg: T): number {
  return arg.length
}

getLength('hello') // OK
getLength([1, 2, 3]) // OK
getLength(42) // Error: number has no length

// keyof constraint
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key]
}

const user = { name: 'Alice', age: 30 }
getProperty(user, 'name') // string
getProperty(user, 'age') // number
getProperty(user, 'foo') // Error: 'foo' is not keyof User

Generic Defaults

interface PaginatedResponse<T, Meta = { page: number; total: number }> {
  data: T[]
  meta: Meta
}

// Using Meta default
const response: PaginatedResponse<User> = {
  data: [{ name: 'Alice', age: 30 }],
  meta: { page: 1, total: 100 },
}

// Customizing Meta
const cursorResponse: PaginatedResponse<User, { cursor: string; hasMore: boolean }> = {
  data: [{ name: 'Bob', age: 25 }],
  meta: { cursor: 'abc123', hasMore: true },
}

Practical Example: API Response Wrapper

// API response type system
interface ApiResponse<T> {
  success: true
  data: T
  timestamp: number
}

interface ApiError {
  success: false
  error: {
    code: string
    message: string
  }
  timestamp: number
}

type ApiResult<T> = ApiResponse<T> | ApiError

// Type-safe API client
async function fetchApi<T>(url: string): Promise<ApiResult<T>> {
  const response = await fetch(url)
  return response.json()
}

// Usage
interface User {
  id: number
  name: string
  email: string
}

const result = await fetchApi<User>('/api/users/1')

if (result.success) {
  console.log(result.data.name) // Type-safe: User
} else {
  console.error(result.error.message) // Type-safe: ApiError
}

Practical Example: Repository Pattern

interface Entity {
  id: string
  createdAt: Date
  updatedAt: Date
}

interface Repository<T extends Entity> {
  findById(id: string): Promise<T | null>
  findMany(filter: Partial<T>): Promise<T[]>
  create(data: Omit<T, 'id' | 'createdAt' | 'updatedAt'>): Promise<T>
  update(id: string, data: Partial<Omit<T, 'id' | 'createdAt' | 'updatedAt'>>): Promise<T>
  delete(id: string): Promise<void>
}

// Implementation
interface User extends Entity {
  name: string
  email: string
  role: 'admin' | 'user'
}

class UserRepository implements Repository<User> {
  async findById(id: string): Promise<User | null> {
    return null
  }

  async findMany(filter: Partial<User>): Promise<User[]> {
    return []
  }

  async create(data: Omit<User, 'id' | 'createdAt' | 'updatedAt'>): Promise<User> {
    return {} as User
  }

  async update(
    id: string,
    data: Partial<Omit<User, 'id' | 'createdAt' | 'updatedAt'>>
  ): Promise<User> {
    return {} as User
  }

  async delete(id: string): Promise<void> {}
}

Practical Example: Builder Pattern

class QueryBuilder<T extends Record<string, unknown>> {
  private conditions: string[] = []
  private selectedFields: string[] = []

  select<K extends keyof T>(...fields: K[]): QueryBuilder<Pick<T, K>> {
    this.selectedFields = fields as string[]
    return this as unknown as QueryBuilder<Pick<T, K>>
  }

  where<K extends keyof T>(field: K, value: T[K]): this {
    this.conditions.push(`${String(field)} = ${JSON.stringify(value)}`)
    return this
  }

  build(): { fields: string[]; conditions: string[] } {
    return {
      fields: this.selectedFields,
      conditions: this.conditions,
    }
  }
}

// Usage
interface Product {
  id: number
  name: string
  price: number
  category: string
}

const query = new QueryBuilder<Product>()
  .select('name', 'price')
  .where('category', 'electronics')
  .build()

Generic Inference (infer) Deep Dive

// Extract function return type
type ReturnTypeOf<T> = T extends (...args: any[]) => infer R ? R : never

// Extract Promise inner type
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T

// Extract array element type
type ElementOf<T> = T extends (infer E)[] ? E : never

// Practical: Using multiple infer together
type FunctionParts<T> = T extends (...args: infer A) => infer R ? { args: A; return: R } : never

type Parts = FunctionParts<(name: string, age: number) => boolean>
// { args: [string, number]; return: boolean }

3. Conditional Types Deep Dive

Extracting Types with the infer Keyword

The infer keyword in Conditional Types serves to "capture" types. It's essential for extracting desired parts from complex types.

// Extract first parameter type of a function
type FirstParam<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never

type P = FirstParam<(name: string, age: number) => void> // string

// Extract constructor's instance type
type InstanceOf<T> = T extends new (...args: any[]) => infer I ? I : never

class MyClass {
  value = 42
}

type Instance = InstanceOf<typeof MyClass> // MyClass

Recursive Conditional Types

// Deep Readonly
type DeepReadonly<T> = T extends (infer U)[]
  ? ReadonlyArray<DeepReadonly<U>>
  : T extends object
    ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
    : T

interface Config {
  db: {
    host: string
    port: number
    credentials: {
      user: string
      password: string
    }
  }
  features: string[]
}

type ReadonlyConfig = DeepReadonly<Config>
// All nested properties become readonly

// Deep Partial
type DeepPartial<T> = T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T

// JSON type (recursive)
type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue }

Distributive Conditional Types

When Conditional Types are applied to union types, they distribute across each member individually.

// Distribution applies
type ToArray<T> = T extends any ? T[] : never

type Result = ToArray<string | number>
// string[] | number[] (distributed)

// Prevent distribution by wrapping in brackets
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never

type Result2 = ToArrayNonDist<string | number>
// (string | number)[] (not distributed)

Practical: Extracting Data Types from API Responses

// API endpoint type definitions
interface ApiEndpoints {
  '/users': { response: User[]; params: never }
  '/users/:id': { response: User; params: { id: string } }
  '/posts': { response: Post[]; params: never }
  '/posts/:id': { response: Post; params: { id: string } }
}

// Extract response type
type ResponseOf<T extends keyof ApiEndpoints> = ApiEndpoints[T]['response']
type ParamsOf<T extends keyof ApiEndpoints> = ApiEndpoints[T]['params']

// Usage
type UserListResponse = ResponseOf<'/users'> // User[]
type UserParams = ParamsOf<'/users/:id'> // { id: string }

Promise Unwrap (Deep Unwrapping)

type DeepAwaited<T> = T extends Promise<infer U> ? DeepAwaited<U> : T

type A = DeepAwaited<Promise<Promise<Promise<string>>>> // string

// Practical: Get the actual return type of an async function
type AsyncReturnType<T extends (...args: any[]) => Promise<any>> = T extends (
  ...args: any[]
) => Promise<infer R>
  ? R
  : never

async function fetchUser(): Promise<User> {
  return {} as User
}

type FetchedUser = AsyncReturnType<typeof fetchUser> // User

4. Mapped Types and Template Literal Types

Implementing Built-in Utility Types

Understanding how TypeScript's built-in utility types work enables you to create custom utilities.

// Partial implementation
type MyPartial<T> = {
  [K in keyof T]?: T[K]
}

// Required implementation
type MyRequired<T> = {
  [K in keyof T]-?: T[K]
}

// Readonly implementation
type MyReadonly<T> = {
  readonly [K in keyof T]: T[K]
}

// Pick implementation
type MyPick<T, K extends keyof T> = {
  [P in K]: T[P]
}

// Omit implementation
type MyOmit<T, K extends keyof T> = {
  [P in keyof T as P extends K ? never : P]: T[P]
}

Key Remapping (as)

In TypeScript 4.1+, the as clause enables key transformation.

// Add prefix to all keys
type Prefixed<T, Prefix extends string> = {
  [K in keyof T as `${Prefix}${Capitalize<string & K>}`]: T[K]
}

interface User {
  name: string
  age: number
}

type PrefixedUser = Prefixed<User, 'get'>
// { getName: string; getAge: number }

// Auto-generate getter types
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
}

type UserGetters = Getters<User>
// { getName: () => string; getAge: () => number }

// Filter keys by type
type StringKeys<T> = {
  [K in keyof T as T[K] extends string ? K : never]: T[K]
}

interface Mixed {
  name: string
  age: number
  email: string
  active: boolean
}

type OnlyStrings = StringKeys<Mixed>
// { name: string; email: string }

Template Literal Types

Combine string literal types to create powerful types.

// Basic template literal
type EventName = 'click' | 'focus' | 'blur'
type EventHandler = `on${Capitalize<EventName>}`
// 'onClick' | 'onFocus' | 'onBlur'

// CSS units
type CSSUnit = 'px' | 'em' | 'rem' | '%' | 'vh' | 'vw'
type CSSValue = `${number}${CSSUnit}`

const width: CSSValue = '100px' // OK
const height: CSSValue = '50vh' // OK

// Built-in string manipulation types
type Upper = Uppercase<'hello'> // 'HELLO'
type Lower = Lowercase<'HELLO'> // 'hello'
type Cap = Capitalize<'hello'> // 'Hello'
type Uncap = Uncapitalize<'Hello'> // 'hello'

Practical: Auto-generating Event Handler Types

interface Events {
  userCreated: { userId: string; name: string }
  userUpdated: { userId: string; changes: Record<string, unknown> }
  orderPlaced: { orderId: string; total: number }
  orderCancelled: { orderId: string; reason: string }
}

// Auto-generate event listener types
type EventListeners<T> = {
  [K in keyof T as `on${Capitalize<string & K>}`]: (payload: T[K]) => void
}

type AppEventListeners = EventListeners<Events>
// {
//   onUserCreated: (payload: { userId: string; name: string }) => void;
//   onUserUpdated: (payload: { userId: string; changes: ... }) => void;
//   onOrderPlaced: (payload: { orderId: string; total: number }) => void;
//   onOrderCancelled: (payload: { orderId: string; reason: string }) => void;
// }

Practical: Auto-generating API Route Types

// REST API method definitions
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'

type ApiRoute<Method extends HttpMethod, Path extends string, Body = never, Response = unknown> = {
  method: Method
  path: Path
  body: Body
  response: Response
}

// Route definitions
type Routes = {
  listUsers: ApiRoute<'GET', '/api/users', never, User[]>
  getUser: ApiRoute<'GET', '/api/users/:id', never, User>
  createUser: ApiRoute<'POST', '/api/users', Omit<User, 'id'>, User>
  updateUser: ApiRoute<'PUT', '/api/users/:id', Partial<User>, User>
  deleteUser: ApiRoute<'DELETE', '/api/users/:id', never, void>
}

// Generate type-safe client
type TypeSafeClient<R extends Record<string, ApiRoute<any, any, any, any>>> = {
  [K in keyof R]: R[K]['body'] extends never
    ? () => Promise<R[K]['response']>
    : (body: R[K]['body']) => Promise<R[K]['response']>
}

type Client = TypeSafeClient<Routes>

5. Branded Types and Phantom Types

The Problem: Limitations of Structural Typing

TypeScript uses structural typing. Types with the same structure are treated as the same type.

// Problem scenario
type UserId = string
type PostId = string

function deleteUser(id: UserId): void {}
function deletePost(id: PostId): void {}

const userId: UserId = 'user-123'
const postId: PostId = 'post-456'

// Both are string, so accidentally swapping them causes no error!
deleteUser(postId) // Should error but passes
deletePost(userId) // Also passes

Solution: Branded Types

// Add a brand symbol for structural distinction
type Brand<T, B extends string> = T & { readonly __brand: B }

type UserId = Brand<string, 'UserId'>
type PostId = Brand<string, 'PostId'>
type OrderId = Brand<string, 'OrderId'>

// Creation functions
function createUserId(id: string): UserId {
  return id as UserId
}

function createPostId(id: string): PostId {
  return id as PostId
}

// Now type-safe!
function deleteUser(id: UserId): void {}
function deletePost(id: PostId): void {}

const userId = createUserId('user-123')
const postId = createPostId('post-456')

deleteUser(userId) // OK
deleteUser(postId) // Error! PostId is not assignable to UserId

Currency Type Safety

type Currency<C extends string> = number & { readonly __currency: C }

type USD = Currency<'USD'>
type EUR = Currency<'EUR'>
type KRW = Currency<'KRW'>

function usd(amount: number): USD {
  return amount as USD
}

function eur(amount: number): EUR {
  return amount as EUR
}

function addUSD(a: USD, b: USD): USD {
  return (a + b) as USD
}

const price1 = usd(100)
const price2 = usd(50)
const priceEur = eur(80)

addUSD(price1, price2) // OK
addUSD(price1, priceEur) // Error! EUR is not assignable to USD

Database ID Type Separation

// Generic branded ID system
type EntityId<Entity extends string> = Brand<string, Entity>

type UserId = EntityId<'User'>
type PostId = EntityId<'Post'>
type CommentId = EntityId<'Comment'>

// Type-safe relationship definitions
interface Post {
  id: PostId
  authorId: UserId
  title: string
  content: string
}

interface Comment {
  id: CommentId
  postId: PostId
  authorId: UserId
  text: string
}

// Usage in repositories
function findPostsByAuthor(authorId: UserId): Promise<Post[]> {
  return Promise.resolve([])
}

function findCommentsByPost(postId: PostId): Promise<Comment[]> {
  return Promise.resolve([])
}

// Prevents incorrect ID usage at compile time
const userId = 'user-1' as UserId
const postId = 'post-1' as PostId

findPostsByAuthor(userId) // OK
findPostsByAuthor(postId) // Error!

Combining with Zod

import { z } from 'zod'

// Apply branded types to Zod schemas
const UserIdSchema = z.string().uuid().brand('UserId')
type UserId = z.infer<typeof UserIdSchema>

const PostIdSchema = z.string().uuid().brand('PostId')
type PostId = z.infer<typeof PostIdSchema>

// Runtime validation + type safety
const userId = UserIdSchema.parse('550e8400-e29b-41d4-a716-446655440000')
// userId type is string & Brand<'UserId'>

// Invalid format throws runtime error
try {
  const invalid = UserIdSchema.parse('not-a-uuid')
} catch (e) {
  console.error('Invalid UserId')
}

6. Ten Type-Safe Patterns

Pattern 1: Discriminated Unions for State Machines

// Express a state machine with types
type RequestState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: Error }

function renderUser(state: RequestState<User>): string {
  switch (state.status) {
    case 'idle':
      return 'Waiting...'
    case 'loading':
      return 'Loading...'
    case 'success':
      return `User: ${state.data.name}`
    case 'error':
      return `Error: ${state.error.message}`
  }
}

// Finite state machine
type TrafficLight =
  | { state: 'red'; timer: number }
  | { state: 'yellow'; timer: number }
  | { state: 'green'; timer: number }

function nextState(light: TrafficLight): TrafficLight {
  switch (light.state) {
    case 'red':
      return { state: 'green', timer: 30 }
    case 'green':
      return { state: 'yellow', timer: 5 }
    case 'yellow':
      return { state: 'red', timer: 30 }
  }
}

Pattern 2: Exhaustive Checking with never

// Verify all cases are handled at compile time
function assertNever(value: never): never {
  throw new Error(`Unexpected value: ${value}`)
}

type Shape =
  | { kind: 'circle'; radius: number }
  | { kind: 'rectangle'; width: number; height: number }
  | { kind: 'triangle'; base: number; height: number }

function area(shape: Shape): number {
  switch (shape.kind) {
    case 'circle':
      return Math.PI * shape.radius ** 2
    case 'rectangle':
      return shape.width * shape.height
    case 'triangle':
      return (shape.base * shape.height) / 2
    default:
      return assertNever(shape)
    // Compile error when a new Shape is added
  }
}

Pattern 3: Type-safe Event Emitter

type EventMap = Record<string, any>

class TypedEventEmitter<T extends EventMap> {
  private listeners: Partial<Record<keyof T, Function[]>> = {}

  on<K extends keyof T>(event: K, listener: (payload: T[K]) => void): void {
    if (!this.listeners[event]) {
      this.listeners[event] = []
    }
    this.listeners[event]!.push(listener)
  }

  emit<K extends keyof T>(event: K, payload: T[K]): void {
    this.listeners[event]?.forEach((fn) => fn(payload))
  }

  off<K extends keyof T>(event: K, listener: (payload: T[K]) => void): void {
    this.listeners[event] = this.listeners[event]?.filter((fn) => fn !== listener)
  }
}

// Usage
interface AppEvents {
  userLoggedIn: { userId: string; timestamp: Date }
  orderPlaced: { orderId: string; total: number }
  error: { message: string; stack?: string }
}

const emitter = new TypedEventEmitter<AppEvents>()

emitter.on('userLoggedIn', (payload) => {
  console.log(payload.userId) // Type-safe: string
})

emitter.emit('orderPlaced', { orderId: '123', total: 99.99 }) // OK
emitter.emit('orderPlaced', { orderId: '123' }) // Error: total missing

Pattern 4: Builder Pattern with Type Accumulation

// Track required fields at the type level
type RequiredFields = 'host' | 'port' | 'database'

class ConnectionBuilder<Built extends string = never> {
  private config: Record<string, unknown> = {}

  host(value: string): ConnectionBuilder<Built | 'host'> {
    this.config.host = value
    return this as any
  }

  port(value: number): ConnectionBuilder<Built | 'port'> {
    this.config.port = value
    return this as any
  }

  database(value: string): ConnectionBuilder<Built | 'database'> {
    this.config.database = value
    return this as any
  }

  username(value: string): ConnectionBuilder<Built> {
    this.config.username = value
    return this as any
  }

  // build is only available when all required fields are set
  build(this: ConnectionBuilder<RequiredFields>): Record<string, unknown> {
    return this.config
  }
}

// Usage
new ConnectionBuilder().host('localhost').port(5432).database('mydb').build() // OK

new ConnectionBuilder().host('localhost').port(5432).build() // Error! database is missing

Pattern 5: Const Assertions and Readonly Tuples

// Preserve literal types with as const
const ROLES = ['admin', 'editor', 'viewer'] as const
type Role = (typeof ROLES)[number] // 'admin' | 'editor' | 'viewer'

// Apply to configuration objects
const CONFIG = {
  api: {
    baseUrl: 'https://api.example.com',
    timeout: 5000,
    retries: 3,
  },
  features: {
    darkMode: true,
    notifications: false,
  },
} as const

type Config = typeof CONFIG
// All values are fixed as literal types

// Routing table
const routes = {
  home: '/',
  about: '/about',
  users: '/users',
  userDetail: '/users/:id',
} as const

type RoutePath = (typeof routes)[keyof typeof routes]
// '/' | '/about' | '/users' | '/users/:id'

Pattern 6: Variadic Tuple Types

// Combining tuple types
type Concat<A extends unknown[], B extends unknown[]> = [...A, ...B]

type AB = Concat<[1, 2], [3, 4]> // [1, 2, 3, 4]

// pipe function type
function pipe<A, B>(fn1: (a: A) => B): (a: A) => B
function pipe<A, B, C>(fn1: (a: A) => B, fn2: (b: B) => C): (a: A) => C
function pipe<A, B, C, D>(fn1: (a: A) => B, fn2: (b: B) => C, fn3: (c: C) => D): (a: A) => D
function pipe(...fns: Function[]) {
  return (x: any) => fns.reduce((acc, fn) => fn(acc), x)
}

const transform = pipe(
  (x: string) => x.length,
  (x: number) => x * 2,
  (x: number) => x.toString()
)

const result = transform('hello') // '10'

Pattern 7: Type-safe ORM Queries

// Type-safe WHERE clause
type WhereClause<T> = {
  [K in keyof T]?: T[K] | { eq: T[K] } | { ne: T[K] } | { gt: T[K] } | { lt: T[K] } | { in: T[K][] }
}

type OrderBy<T> = {
  [K in keyof T]?: 'asc' | 'desc'
}

interface QueryOptions<T> {
  where?: WhereClause<T>
  orderBy?: OrderBy<T>
  limit?: number
  offset?: number
}

function findMany<T>(table: string, options: QueryOptions<T>): Promise<T[]> {
  return Promise.resolve([])
}

// Usage
interface Product {
  id: number
  name: string
  price: number
  category: string
}

findMany<Product>('products', {
  where: {
    price: { gt: 100 },
    category: { in: ['electronics', 'books'] },
  },
  orderBy: { price: 'desc' },
  limit: 10,
})

Pattern 8: Recursive Types (JSON, Tree)

// JSON type
type Json = string | number | boolean | null | Json[] | { [key: string]: Json }

// Type-safe tree structure
interface TreeNode<T> {
  value: T
  children: TreeNode<T>[]
}

// Nested object path type
type NestedKeyOf<T extends object> = {
  [K in keyof T & string]: T[K] extends object ? K | `${K}.${NestedKeyOf<T[K]>}` : K
}[keyof T & string]

interface DeepObj {
  user: {
    profile: {
      name: string
      avatar: string
    }
    settings: {
      theme: string
    }
  }
}

type Paths = NestedKeyOf<DeepObj>
// 'user' | 'user.profile' | 'user.profile.name' | 'user.profile.avatar' | 'user.settings' | 'user.settings.theme'

Pattern 9: HKT (Higher-Kinded Types) Emulation

TypeScript doesn't directly support higher-kinded types, but they can be emulated.

// HKT interface
interface HKT {
  readonly type: unknown
}

// Functor interface emulation
interface Functor<F extends HKT> {
  map<A, B>(fa: Kind<F, A>, f: (a: A) => B): Kind<F, B>
}

// Kind type helper
type Kind<F extends HKT, A> = F extends { readonly type: unknown }
  ? (F & { readonly type: A })['type']
  : never

// Array Functor
interface ArrayHKT extends HKT {
  readonly type: Array<this['type']>
}

// Option Functor
type Option<A> = { tag: 'some'; value: A } | { tag: 'none' }

interface OptionHKT extends HKT {
  readonly type: Option<this['type']>
}

Pattern 10: Module Augmentation and Declaration Merging

// Extend existing modules
declare module 'express' {
  interface Request {
    user?: {
      id: string
      role: string
    }
    requestId: string
  }
}

// Interface merging (Declaration Merging)
interface Window {
  __APP_CONFIG__: {
    apiUrl: string
    version: string
  }
}

// Extend existing enums
enum Color {
  Red = 'RED',
  Blue = 'BLUE',
}

// Extension via namespace
namespace Color {
  export function fromHex(hex: string): Color {
    return Color.Red
  }
}

Color.fromHex('#ff0000') // OK

7. Zod + tRPC: Runtime + Compile-time Type Safety

Zod Schemas for Runtime Validation + Type Inference

Zod uses a "schema-first" approach where a single schema provides both runtime validation and TypeScript types.

import { z } from 'zod'

// Schema definition = type definition + runtime validation
const UserSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(2).max(50),
  email: z.string().email(),
  age: z.number().int().min(0).max(150),
  role: z.enum(['admin', 'user', 'moderator']),
  preferences: z.object({
    theme: z.enum(['light', 'dark']).default('light'),
    notifications: z.boolean().default(true),
    language: z.string().optional(),
  }),
  createdAt: z.coerce.date(),
})

// Automatic type inference
type User = z.infer<typeof UserSchema>

// Runtime validation
const result = UserSchema.safeParse(unknownData)
if (result.success) {
  console.log(result.data.name) // Type-safe
} else {
  console.error(result.error.issues) // Detailed error info
}

Advanced Zod Patterns

// Discriminated unions
const EventSchema = z.discriminatedUnion('type', [
  z.object({
    type: z.literal('click'),
    x: z.number(),
    y: z.number(),
  }),
  z.object({
    type: z.literal('keypress'),
    key: z.string(),
    modifiers: z.array(z.string()),
  }),
  z.object({
    type: z.literal('scroll'),
    deltaX: z.number(),
    deltaY: z.number(),
  }),
])

type AppEvent = z.infer<typeof EventSchema>

// Type transformation with transform
const DateFromString = z.string().transform((str) => new Date(str))
type ParsedDate = z.infer<typeof DateFromString> // Date

// Recursive schemas
type Category = z.infer<typeof CategorySchema>
const CategorySchema: z.ZodType<Category> = z.lazy(() =>
  z.object({
    name: z.string(),
    subcategories: z.array(CategorySchema),
  })
)

tRPC: Full-stack Type-safe APIs

tRPC provides complete type safety between server and client without any code generation.

// Server router definition
import { initTRPC } from '@trpc/server'

const t = initTRPC.create()

const appRouter = t.router({
  user: t.router({
    getById: t.procedure.input(z.object({ id: z.string().uuid() })).query(async ({ input }) => {
      const user = await db.user.findUnique({ where: { id: input.id } })
      return user
    }),

    create: t.procedure
      .input(
        z.object({
          name: z.string().min(2),
          email: z.string().email(),
          role: z.enum(['admin', 'user']),
        })
      )
      .mutation(async ({ input }) => {
        return await db.user.create({ data: input })
      }),

    list: t.procedure
      .input(
        z.object({
          page: z.number().int().min(1).default(1),
          limit: z.number().int().min(1).max(100).default(20),
          role: z.enum(['admin', 'user']).optional(),
        })
      )
      .query(async ({ input }) => {
        return await db.user.findMany({
          skip: (input.page - 1) * input.limit,
          take: input.limit,
          where: input.role ? { role: input.role } : undefined,
        })
      }),
  }),
})

export type AppRouter = typeof appRouter

React Query + tRPC Integration

// On the client — complete type safety
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '../server/router';

const trpc = createTRPCReact<AppRouter>();

function UserProfile({ userId }: { userId: string }) {
  const userQuery = trpc.user.getById.useQuery({ id: userId });

  if (userQuery.isLoading) return <div>Loading...</div>;
  if (userQuery.error) return <div>Error occurred</div>;

  // userQuery.data type is automatically inferred from server
  return <div>{userQuery.data.name}</div>;
}

function CreateUserForm() {
  const createUser = trpc.user.create.useMutation({
    onSuccess: (data) => {
      console.log(`User created: ${data.name}`);
    },
  });

  const handleSubmit = (formData: FormData) => {
    createUser.mutate({
      name: formData.get('name') as string,
      email: formData.get('email') as string,
      role: 'user',
    });
  };

  return <form onSubmit={(e) => { e.preventDefault(); handleSubmit(new FormData(e.currentTarget)); }}>...</form>;
}

End-to-end Type Safety Architecture

// Full architecture flow:
// 1. Define Zod schemas (single source of truth)
// 2. Use schemas in tRPC router (server)
// 3. Auto-infer types on client (frontend)
// 4. Runtime validation handled automatically by Zod

// DB schema integration (Drizzle ORM example)
import { pgTable, varchar, integer, timestamp } from 'drizzle-orm/pg-core'

const users = pgTable('users', {
  id: varchar('id', { length: 36 }).primaryKey(),
  name: varchar('name', { length: 50 }).notNull(),
  email: varchar('email', { length: 255 }).notNull().unique(),
  age: integer('age'),
  createdAt: timestamp('created_at').defaultNow(),
})

// Infer types from Drizzle
type DbUser = typeof users.$inferSelect
type NewDbUser = typeof users.$inferInsert

// Sync Zod schema with DB types
const CreateUserInput = z.object({
  name: z.string().min(2).max(50),
  email: z.string().email().max(255),
  age: z.number().int().min(0).optional(),
}) satisfies z.ZodType<Omit<NewDbUser, 'id' | 'createdAt'>>

8. TypeScript Performance Optimization

Optimal tsconfig.json Settings

{
  "compilerOptions": {
    "target": "es2022",
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "strict": true,
    "skipLibCheck": true,
    "incremental": true,
    "tsBuildInfoFile": "./.tsbuildinfo",
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "isolatedModules": true,
    "verbatimModuleSyntax": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "forceConsistentCasingInFileNames": true
  }
}

Type Instantiation Depth Limits

Complex recursive types can hit TypeScript's type instantiation depth limit (default 50).

// Bad: infinite recursion risk
type DeepNested<T, Depth extends number[] = []> = Depth['length'] extends 10
  ? T
  : { value: T; nested: DeepNested<T, [...Depth, 0]> }

// Good: limit recursion depth
type MaxDepth = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

type SafeDeepPartial<T, D extends number = 5> = [D] extends [never]
  ? T
  : T extends object
    ? { [K in keyof T]?: SafeDeepPartial<T[K], MaxDepth[D]> }
    : T

Improving Build Speed with Project References

In large monorepos, Project References rebuild only changed projects.

{
  "references": [
    { "path": "./packages/shared" },
    { "path": "./packages/api" },
    { "path": "./packages/web" }
  ],
  "compilerOptions": {
    "composite": true,
    "incremental": true
  }
}

Per-package tsconfig.json:

{
  "compilerOptions": {
    "composite": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "declaration": true,
    "declarationMap": true
  },
  "references": [{ "path": "../shared" }]
}

Build commands:

# Build following project references
tsc --build

# Incremental build (only changed)
tsc --build --incremental

# Clean build
tsc --build --clean

SWC vs esbuild Transpiler Comparison

AspecttscSWCesbuild
LanguageTypeScriptRustGo
Type checkingYesNoNo
Speed (1000 files)~10s~0.5s~0.3s
BundlingNoYes (SWC Pack)Yes
DecoratorsFull supportPartialLimited
Recommended forCI type checkingNext.js, JestVite, bundling
{
  "scripts": {
    "typecheck": "tsc --noEmit",
    "build": "esbuild src/index.ts --bundle --outdir=dist",
    "dev": "tsx watch src/index.ts"
  }
}

9. Type Gymnastics (Type Challenges) Practice

Type gymnastics means implementing logic using only TypeScript's type system. It's immensely helpful when dealing with complex types in practice.

Easy Level

// 1. MyPick implementation
type MyPick<T, K extends keyof T> = {
  [P in K]: T[P]
}

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

type TodoPreview = MyPick<Todo, 'title' | 'completed'>
// { title: string; completed: boolean }

// 2. MyReadonly implementation
type MyReadonly<T> = {
  readonly [K in keyof T]: T[K]
}

// 3. Tuple to Object
type TupleToObject<T extends readonly (string | number | symbol)[]> = {
  [K in T[number]]: K
}

const tuple = ['tesla', 'model 3', 'model X'] as const
type TupleObj = TupleToObject<typeof tuple>

// 4. First of Array
type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never

type A = First<[3, 2, 1]> // 3
type B = First<[]> // never

// 5. Length of Tuple
type Length<T extends readonly any[]> = T['length']

type L = Length<[1, 2, 3]> // 3

Medium Level

// 1. Deep Readonly
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object
    ? T[K] extends Function
      ? T[K]
      : DeepReadonly<T[K]>
    : T[K]
}

// 2. Chainable Options
type Chainable<T = {}> = {
  option<K extends string, V>(
    key: K extends keyof T ? never : K,
    value: V
  ): Chainable<T & Record<K, V>>
  get(): T
}

declare const config: Chainable
const result = config
  .option('foo', 123)
  .option('name', 'hello')
  .option('bar', { value: 'world' })
  .get()

// 3. Trim
type TrimLeft<S extends string> = S extends `${' ' | '\n' | '\t'}${infer R}` ? TrimLeft<R> : S

type TrimRight<S extends string> = S extends `${infer R}${' ' | '\n' | '\t'}` ? TrimRight<R> : S

type Trim<S extends string> = TrimLeft<TrimRight<S>>

type Trimmed = Trim<'  Hello World  '> // 'Hello World'

// 4. Replace
type Replace<S extends string, From extends string, To extends string> = From extends ''
  ? S
  : S extends `${infer L}${From}${infer R}`
    ? `${L}${To}${R}`
    : S

type Replaced = Replace<'hello world', 'world', 'TypeScript'>

// 5. Flatten
type Flatten<T extends any[]> = T extends [infer First, ...infer Rest]
  ? First extends any[]
    ? [...Flatten<First>, ...Flatten<Rest>]
    : [First, ...Flatten<Rest>]
  : []

type Flat = Flatten<[1, [2, [3, [4]]]]> // [1, 2, 3, 4]

Hard Level

// 1. Curry (simplified version)
type Curry<F> = F extends (...args: infer A) => infer R
  ? A extends [infer First, ...infer Rest]
    ? (arg: First) => Rest extends [] ? R : Curry<(...args: Rest) => R>
    : R
  : never

declare function curry<F extends (...args: any[]) => any>(fn: F): Curry<F>

function add(a: number, b: number, c: number): number {
  return a + b + c
}

const curriedAdd = curry(add)
const result2 = curriedAdd(1)(2)(3) // number

// 2. String Parser (simplified path parser)
type ParsePath<S extends string> = S extends `${string}:${infer Param}/${infer Rest}`
  ? { [K in Param]: string } & ParsePath<Rest>
  : S extends `${string}:${infer Param}`
    ? { [K in Param]: string }
    : {}

type Params = ParsePath<'/users/:userId/posts/:postId'>
// { userId: string } & { postId: string }

// 3. IsUnion
type IsUnion<T, U = T> = T extends U ? ([U] extends [T] ? false : true) : never

type A1 = IsUnion<string | number> // true
type A2 = IsUnion<string> // false

Beginner (1-2 weeks)

  1. Basic types, interfaces, generics fundamentals
  2. Union Types, Intersection Types
  3. Type Guards, Narrowing

Intermediate (2-4 weeks)

  1. Conditional Types, Mapped Types
  2. Template Literal Types
  3. Understanding built-in utility type internals

Advanced (4-8 weeks)

  1. Recursive types, Variadic Tuple Types
  2. Branded Types, Phantom Types
  3. HKT emulation
  4. type-challenges repository exercises

Master (ongoing)

  1. Compiler API usage
  2. Writing custom transformers
  3. Language Service Plugin development

10. Quiz

Q1: What is the main purpose of the NoInfer utility type?

NoInfer is a utility type that controls whether a specific parameter participates in type inference for generic functions. This prevents unintended type widening. For example, it is used when you want the default value parameter to not influence the inference of the generic type T.

Q2: How do you prevent distribution in Distributive Conditional Types?

To prevent union distribution in Conditional Types, wrap both sides of extends in brackets. For example, instead of T extends any ? T[] : never, write [T] extends [any] ? T[] : never. This ensures the union type is processed as a whole rather than being distributed individually.

Q3: Why are Branded Types necessary?

In TypeScript's structural typing system, types with the same structure are compatible. For example, if UserId and PostId are both strings, accidentally swapping them causes no compile error. Branded Types add a unique brand property to distinguish types that are structurally identical but logically different. This prevents incorrect ID usage at compile time.

Q4: What type safety advantages does tRPC provide compared to traditional REST APIs?

With tRPC, router types defined on the server are automatically inferred on the client. Without any code generation (codegen) process, server input/output types provide complete autocompletion and type checking on the client. When combined with Zod schemas, runtime validation is also handled automatically, achieving end-to-end type safety.

Q5: What are the main advantages of TypeScript Project References?

Project References optimize build performance in large monorepos. Key advantages include: First, incremental builds that only rebuild changed projects. Second, explicit dependency declaration between projects enabling automatic build order management. Third, when used with the composite option, declaration files are cached for each project, eliminating the need to re-parse source files when referenced by other projects.


11. References

  1. TypeScript Official Documentation — Handbook
  2. TypeScript 5.4 Release Notes
  3. TypeScript 5.5 Release Notes
  4. TypeScript 5.6 Release Notes
  5. TypeScript 5.7 Release Notes
  6. type-challenges GitHub
  7. Zod Official Documentation
  8. tRPC Official Documentation
  9. Total TypeScript — Matt Pocock
  10. TypeScript Deep Dive — Basarat
  11. Effect-TS — Type-safe Functional Programming
  12. Drizzle ORM — Type-safe ORM
  13. SWC Official Documentation
  14. esbuild Official Documentation
  15. TypeScript Performance Wiki
  16. Branded Types in TypeScript — Kent C. Dodds
  17. Why TypeScript's Type System is Turing Complete