DEV Community

Anup Karanjkar
Anup Karanjkar

Posted on • Originally published at wowhow.cloud

TypeScript Utility Types: 15 Advanced Patterns Every Developer Needs

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

// Form data only needs email and role
type UserFormData = Pick

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

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

// Create input omits auto-generated fields
type CreateProductInput = Omit

// Update input makes everything optional except id
type UpdateProductInput = Partial> & Pick
Enter fullscreen mode Exit fullscreen mode

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 = {
  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
const flags: FeatureFlags = { newCheckout: true, betaDashboard: false }
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

5. Extract and Exclude — Filter Union Types

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

// Extract: keep only matching members
type PublishableStatus = Extract
// Result: 'draft' | 'pending_review'

// Exclude: remove matching members
type ActiveStatus = Exclude
// Result: 'draft' | 'published' | 'pending_review'

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

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

// Extract specific function overloads
type EventHandler = ((e: MouseEvent) => void) | ((e: KeyboardEvent) => void) | string
type FunctionHandlers = Extract
// Result: ((e: MouseEvent) => void) | ((e: KeyboardEvent) => void)
Enter fullscreen mode Exit fullscreen mode

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>

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

type ApiResponse = ReturnType>
// { data: T; meta: { page: number; total: number }; timestamp: number }
Enter fullscreen mode Exit fullscreen mode

7. Parameters — Extract Function Arguments

function createOrder(
  userId: string,
  items: Array,
  couponCode?: string,
  shippingAddress?: string
) {
  // implementation
}

type CreateOrderArgs = Parameters
// [userId: string, items: Array, couponCode?: string, shippingAddress?: string]

// Extract individual parameter types
type OrderItems = CreateOrderArgs[1]
// Array

// 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' }
}
Enter fullscreen mode Exit fullscreen mode

8. Readonly — Prevent Mutation

interface AppState {
  user: { id: string; email: string }
  cart: Array
  flags: Record
}

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

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

type FrozenState = DeepReadonly

declare const state: FrozenState
// state.user.email = 'new@example.com'  // Error: Cannot assign to 'email' — it is read-only
Enter fullscreen mode Exit fullscreen mode

9. ConstructorParameters — Infer Class Constructor Args

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

type ApiClientArgs = ConstructorParameters
// [baseUrl: string, apiKey: string, timeout?: number]

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

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
}

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

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

type ProfileGetters = Getters
// { getName: () => string; getEmail: () => string; ... }
Enter fullscreen mode Exit fullscreen mode

11. Conditional Types — Branch on Type Shape

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

type A = IsArray  // true
type B = IsArray    // false

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

type C = UnwrapArray   // string
type D = UnwrapArray     // number

// UnwrapPromise: works with any depth
type Awaited = T extends Promise ? Awaited : T

type E = Awaited>>  // string

// Distribute over unions
type ToArray = T extends unknown ? T[] : never
type StringOrNumberArrays = ToArray  // string[] | number[]
Enter fullscreen mode Exit fullscreen mode

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}`
// 'onClick' | 'onFocus' | 'onBlur' | 'onChange'
Enter fullscreen mode Exit fullscreen mode

13. Discriminated Unions — Safe State Machines

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

function renderState(state: FetchState): 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
Enter fullscreen mode Exit fullscreen mode

14. Infer — Extract Types from Generic Constraints

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

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

// Extract the key type from a Map
type MapKey = T extends Map ? K : never
type MapValue = T extends Map ? V : never

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

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

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

15. Branded Types — Prevent ID Mixups

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

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

type UserId = Brand
type OrderId = Brand
type ProductId = Brand

declare function getOrder(orderId: OrderId): Promise
declare function getUser(userId: UserId): Promise

// 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'
Enter fullscreen mode Exit fullscreen mode

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.

Originally published at wowhow.cloud

Top comments (0)