Skip to main content
Browse all
WOWHOW/FIELD NOTES/DEVELOPER-PRODUCTIVITY·21 MAY 2026·16 MIN READ

TypeScript ships 20+ built-in utility types — but most developers only use Partial and Required. This guide covers 15 patterns that will eliminate entire categories of runtime errors from your codebase.

WW
WOWHOW
FOUNDER · 14YR SHIPPING
Published
21 May 2026
Reading
16 min · 2,100 words
TL;DR

Master TypeScript utility types with 15 advanced patterns: Pick, Omit, Record, Extract, Exclude, ReturnType, custom mapped types, and conditional types. Real-world examples.

TypeScript ships 20+ built-in utility types, and most developers use three. Partial, Required, maybe Pick. The rest sit unused while teams write boilerplate types that TypeScript could derive automatically — or worse, use any to avoid the type work entirely.

These 15 patterns cover the utility types that eliminate real bugs: wrong field names in API responses, missing properties after refactors, unsafe casts across module boundaries. Each example is production-ready TypeScript that compiles with strict: true.

For TypeScript-heavy tools and templates, browse our developer productivity catalog.

Try it yourself: Free JSON Formatter & Validator — free, no signup, runs in your browser.

1. Pick — Extract a Subset of an Interface

interface User {
  id: string
  email: string
  passwordHash: string
  role: 'admin' | 'user'
  createdAt: Date
  updatedAt: Date
}

// Never expose passwordHash in API responses
type PublicUser = Pick<User, 'id' | 'email' | 'role' | 'createdAt'>

// Form data only needs email and role
type UserFormData = Pick<User, 'email' | 'role'>

function formatUserResponse(user: User): PublicUser {
  const { passwordHash: _, ...publicFields } = user
  return publicFields as PublicUser
}

2. Omit — Remove Fields You Don't Want

interface Product {
  id: string
  name: string
  price: number
  internalCostPrice: number   // never expose
  supplierCode: string         // never expose
  description: string
  categoryId: string
}

type PublicProduct = Omit<Product, 'internalCostPrice' | 'supplierCode'>

// Create input omits auto-generated fields
type CreateProductInput = Omit<Product, 'id'>

// Update input makes everything optional except id
type UpdateProductInput = Partial<Omit<Product, 'id'>> & Pick<Product, 'id'>

3. Record — Build Typed Dictionaries

type Category = 'tools' | 'templates' | 'courses' | 'plugins'

interface CategoryMeta {
  displayName: string
  slug: string
  productCount: number
}

// Keys MUST be every Category member — missing one is a compile error
const categoryRegistry: Record<Category, CategoryMeta> = {
  tools:     { displayName: 'Tools',     slug: 'tools',     productCount: 117 },
  templates: { displayName: 'Templates', slug: 'templates', productCount: 843 },
  courses:   { displayName: 'Courses',   slug: 'courses',   productCount: 204 },
  plugins:   { displayName: 'Plugins',   slug: 'plugins',   productCount: 89 },
}

// Record with string keys for dynamic data
type FeatureFlags = Record<string, boolean>
const flags: FeatureFlags = { newCheckout: true, betaDashboard: false }

4. Partial and Required Together

interface Config {
  apiUrl: string
  timeout: number
  retries: number
  debug: boolean
  logLevel: 'error' | 'warn' | 'info' | 'debug'
}

// All optional for user-provided overrides
type UserConfig = Partial<Config>

// Merge with defaults to get complete config
function resolveConfig(userConfig: UserConfig): Required<Config> {
  const defaults: Required<Config> = {
    apiUrl: 'https://api.wowhow.cloud',
    timeout: 5000,
    retries: 3,
    debug: false,
    logLevel: 'error',
  }
  return { ...defaults, ...userConfig }
}

// Required<T> strips undefined from all fields — useful for post-validation
interface RawFormInput {
  name?: string
  email?: string
  message?: string
}

function validateForm(input: RawFormInput): Required<RawFormInput> {
  if (!input.name || !input.email || !input.message) {
    throw new Error('All fields required')
  }
  return input as Required<RawFormInput>
}

5. Extract and Exclude — Filter Union Types

type Status = 'draft' | 'published' | 'archived' | 'deleted' | 'pending_review'

// Extract: keep only matching members
type PublishableStatus = Extract<Status, 'draft' | 'pending_review'>
// Result: 'draft' | 'pending_review'

// Exclude: remove matching members
type ActiveStatus = Exclude<Status, 'archived' | 'deleted'>
// Result: 'draft' | 'published' | 'pending_review'

// Practical: exclude null/undefined from a type
type NonNullable<T> = Exclude<T, null | undefined>  // this IS built-in, shown for clarity

type MaybeString = string | null | undefined
type DefinitelyString = NonNullable<MaybeString>  // string

// Extract specific function overloads
type EventHandler = ((e: MouseEvent) => void) | ((e: KeyboardEvent) => void) | string
type FunctionHandlers = Extract<EventHandler, Function>
// Result: ((e: MouseEvent) => void) | ((e: KeyboardEvent) => void)

6. ReturnType — Infer a Function's Return Value

async function fetchUserWithOrders(userId: string) {
  const user = await db.users.findUnique({ where: { id: userId } })
  const orders = await db.orders.findMany({ where: { userId } })
  return { user, orders, fetchedAt: new Date() }
}

// No need to manually define this type — infer it
type UserWithOrders = Awaited<ReturnType<typeof fetchUserWithOrders>>

// Useful when the return type is complex or changes frequently
function createApiResponse<T>(data: T, meta: { page: number; total: number }) {
  return { data, meta, timestamp: Date.now() }
}

type ApiResponse<T> = ReturnType<typeof createApiResponse<T>>
// { data: T; meta: { page: number; total: number }; timestamp: number }

7. Parameters — Extract Function Arguments

function createOrder(
  userId: string,
  items: Array<{ productId: string; quantity: number }>,
  couponCode?: string,
  shippingAddress?: string
) {
  // implementation
}

type CreateOrderArgs = Parameters<typeof createOrder>
// [userId: string, items: Array<...>, couponCode?: string, shippingAddress?: string]

// Extract individual parameter types
type OrderItems = CreateOrderArgs[1]
// Array<{ productId: string; quantity: number }>

// Useful for building test fixtures that stay in sync with function signatures
function mockCreateOrder(...args: CreateOrderArgs) {
  const [userId, items, couponCode] = args
  return { userId, items, couponCode, id: 'mock-id', status: 'pending' }
}

8. Readonly — Prevent Mutation

interface AppState {
  user: { id: string; email: string }
  cart: Array<{ productId: string; quantity: number }>
  flags: Record<string, boolean>
}

// Deep readonly — mutation attempt is a compile error
type ImmutableState = Readonly<AppState>

// For deep readonly, use a recursive type
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K]
}

type FrozenState = DeepReadonly<AppState>

declare const state: FrozenState
// state.user.email = '[email protected]'  // Error: Cannot assign to 'email' — it is read-only

9. ConstructorParameters — Infer Class Constructor Args

class ApiClient {
  constructor(
    private readonly baseUrl: string,
    private readonly apiKey: string,
    private readonly timeout = 5000
  ) {}
}

type ApiClientArgs = ConstructorParameters<typeof ApiClient>
// [baseUrl: string, apiKey: string, timeout?: number]

// Create factory without repeating the arg types
function createApiClient(...args: ApiClientArgs): ApiClient {
  return new ApiClient(...args)
}

10. Mapped Types — Transform Every Key

interface UserProfile {
  name: string
  email: string
  bio: string
  website: string
}

// Make every field nullable
type NullableProfile = {
  [K in keyof UserProfile]: UserProfile[K] | null
}

// Make every field an async getter
type AsyncProfile = {
  [K in keyof UserProfile]: () => Promise<UserProfile[K]>
}

// Add validation state alongside each field
type FormField<T> = { value: T; error: string | null; touched: boolean }
type ProfileForm = {
  [K in keyof UserProfile]: FormField<UserProfile[K]>
}

// Remap keys with 'as'
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
}

type ProfileGetters = Getters<UserProfile>
// { getName: () => string; getEmail: () => string; ... }

11. Conditional Types — Branch on Type Shape

// IsArray: returns true if T is an array
type IsArray<T> = T extends unknown[] ? true : false

type A = IsArray<string[]>  // true
type B = IsArray<string>    // false

// UnwrapArray: get element type from array, passthrough if not
type UnwrapArray<T> = T extends (infer U)[] ? U : T

type C = UnwrapArray<string[]>   // string
type D = UnwrapArray<number>     // number

// UnwrapPromise: works with any depth
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T

type E = Awaited<Promise<Promise<string>>>  // string

// Distribute over unions
type ToArray<T> = T extends unknown ? T[] : never
type StringOrNumberArrays = ToArray<string | number>  // string[] | number[]

12. Template Literal Types — String Manipulation in the Type System

type HttpMethod = 'get' | 'post' | 'put' | 'delete' | 'patch'
type ApiEndpoint = '/users' | '/products' | '/orders'

// Combine to get all valid route identifiers
type RouteId = `${HttpMethod}:${ApiEndpoint}`
// 'get:/users' | 'get:/products' | 'post:/users' | ...

// CSS property names
type CssProperty = 'margin' | 'padding' | 'border'
type CssDirection = 'top' | 'right' | 'bottom' | 'left'
type CssDirectedProperty = `${CssProperty}-${CssDirection}`
// 'margin-top' | 'margin-right' | ... | 'border-left'

// Event handler naming convention
type EventName = 'click' | 'focus' | 'blur' | 'change'
type HandlerName = `on${Capitalize<EventName>}`
// 'onClick' | 'onFocus' | 'onBlur' | 'onChange'

13. Discriminated Unions — Safe State Machines

type FetchState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: Error; retryCount: number }

function renderState<T>(state: FetchState<T>): string {
  switch (state.status) {
    case 'idle':    return 'Ready to fetch'
    case 'loading': return 'Loading...'
    case 'success': return `Loaded ${JSON.stringify(state.data)}`
    case 'error':   return `Error: ${state.error.message} (retry ${state.retryCount})`
  }
}

// TypeScript enforces exhaustive handling — add a new status and the switch breaks at compile time

14. Infer — Extract Types from Generic Constraints

// Extract the resolved type from a Promise
type UnwrapPromise<T> = T extends Promise<infer R> ? R : T

// Extract the element type from an array
type ElementOf<T> = T extends Array<infer E> ? E : never

// Extract the key type from a Map
type MapKey<T> = T extends Map<infer K, unknown> ? K : never
type MapValue<T> = T extends Map<unknown, infer V> ? V : never

// Extract props from a React component
type PropsOf<T> = T extends React.ComponentType<infer P> ? P : never

// Real usage: derive event payload type from a handler
type HandlerPayload<T> = T extends (payload: infer P) => void ? P : never

type ClickHandler = (payload: { x: number; y: number; target: HTMLElement }) => void
type ClickPayload = HandlerPayload<ClickHandler>
// { x: number; y: number; target: HTMLElement }

15. Branded Types — Prevent ID Mixups

// Without branding, these are all just 'string' — easy to swap accidentally
declare function getOrder(orderId: string): Promise<Order>
declare function getUser(userId: string): Promise<User>

// With branding, mixing them is a compile error
declare const __brand: unique symbol
type Brand<T, B extends string> = T & { [__brand]: B }

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

declare function getOrder(orderId: OrderId): Promise<Order>
declare function getUser(userId: UserId): Promise<User>

// Constructors validate and brand the IDs
function toUserId(raw: string): UserId {
  if (!/^user_[a-z0-9]{20}$/.test(raw)) throw new Error('Invalid user ID')
  return raw as UserId
}

// This now errors at compile time:
// getUser(orderId)  // Argument of type 'OrderId' is not assignable to 'UserId'

People Also Ask

What is the difference between Pick and Extract in TypeScript?

Pick<T, K> works on object types — it creates a new type with only the keys K from object T. Extract<T, U> works on union types — it keeps only the union members assignable to U. Use Pick when you want a subset of an interface's properties; use Extract when you want a subset of a union's members.

When should I use a mapped type vs an interface?

Use interfaces when you know the exact keys at design time and want a named, documentable contract. Use mapped types when the keys are derived programmatically — from another type's keys, from a union, or from template literals. Mapped types are invaluable for keeping related types in sync: if UserProfile changes, ProfileForm and NullableProfile update automatically.

Are TypeScript utility types available at runtime?

No. All TypeScript types — including utility types — are erased at compile time. They exist only during type-checking and provide zero runtime overhead. If you need runtime validation (e.g., for API input), use a schema library like zod or valibot alongside your TypeScript types — they serve different purposes and complement each other.

Tags:TypeScriptutility typesadvanced TypeScriptmapped typesconditional typesTypeScript patternsPickOmitRecordbranded types
All Articles
WW

Written by

WOWHOW

The WOWHOW team brings 14+ years of production engineering experience. Every tool and product in the catalog is personally built, tested, and curated.

Monday Memo · Free

One insight, every Monday. 7am IST. Zero fluff.

1 field report, 3 links, 1 tool we actually use. No fluff, no spam.

Need production-ready templates?

Free browser tools with no signup, plus 2,000+ premium dev templates and starter kits.

Comments · 0

Beta: comments are stored locally on your device and not visible to other readers.

Sign in to join the conversation

No comments yet. Be the first to share your thoughts.

Pairs with this note

More from developer-productivity

See all