Skip to main content

TypeScript Utility Types: A Complete Guide

Master TypeScript utility types including Partial, Required, Pick, Omit, Record, and more. Learn how to write cleaner, type-safe code with practical examples.

10 min read
TypeScript utility types overview showing Partial, Required, Pick, Omit, and Record with code examples

TypeScript’s built-in utility types are powerful tools that help you write cleaner, more maintainable code. In this guide, I’ll walk through the most commonly used utility types with practical examples from real-world applications.

UTILITY TYPES YOU'LL REACH FOR CONSTANTLY

A small subset of built-ins covers most day-to-day type reshaping in frontend and API work. Knowing when to use each one removes a lot of duplication.

PATCHES

`Partial<T>`

Perfect for update flows where callers only send the fields they want to change.

  • Form patches
  • Update DTOs
  • Feature-flagged config overrides

STRICTNESS

`Required<T>`

Useful when a loose input shape becomes a guaranteed runtime shape after defaults or validation.

  • Normalized config
  • Post-validation objects
  • Internal invariants

SHAPING

`Pick<T, K>` and `Omit<T, K>`

The fastest way to carve focused view models and payload types out of larger domain types.

  • Preview cards
  • Create/update payloads
  • Public vs internal fields

MAPS

`Record<K, T>`

Great for dictionaries, lookup tables, and keyed collections where the value shape is consistent.

  • Role maps
  • Route registries
  • Status-to-label maps

SAFETY

`Readonly<T>` and `NonNullable<T>`

These types help lock down accidental mutation and strip nullable cases after checks or normalization.

  • Immutable config objects
  • Derived non-null props
  • Safer shared state

INFERENCE

`ReturnType<T>` and `Awaited<T>`

Use them to extract shapes from real functions instead of manually duplicating types that will drift.

  • Async loader results
  • Factory output types
  • API wrapper return values

Why Utility Types Matter

When building large-scale applications like the ones I work on at Expedia Group, type safety isn’t just nice to have — it’s essential. Utility types help you derive new types from existing ones without duplication.

Partial<T>

Makes all properties of T optional. This is incredibly useful for update functions.

interface User {
  id: string;
  name: string;
  email: string;
  role: 'admin' | 'user';
}

function updateUser(id: string, updates: Partial<User>) {
  // Only update the fields that were provided
}

updateUser('123', { name: 'Umesh' }); // Valid!

Required<T>

The opposite of Partial — makes all properties required.

interface Config {
  host?: string;
  port?: number;
  debug?: boolean;
}

const defaultConfig: Required<Config> = {
  host: 'localhost',
  port: 3000,
  debug: false,
};

Pick<T, K>

Creates a type with only the specified properties.

type UserPreview = Pick<User, 'id' | 'name'>;

// Equivalent to:
// { id: string; name: string }

Omit<T, K>

Creates a type excluding the specified properties.

type CreateUserInput = Omit<User, 'id'>;

// Everything except id

Record<K, T>

Creates a type with keys of type K and values of type T.

type UserRoles = Record<string, User[]>;

const roleMap: UserRoles = {
  admin: [/* admin users */],
  editor: [/* editor users */],
};

Practical Example: API Response Types

Here’s how I combine these utility types in real projects:

interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
}

type UserListResponse = ApiResponse<Pick<User, 'id' | 'name' | 'role'>[]>;
type UserUpdatePayload = Partial<Omit<User, 'id'>>;

A Few More Utility Types Worth Knowing

Readonly<T>

Use Readonly when an object should never be mutated after creation.

interface FeatureFlags {
  newSearch: boolean;
  redesignedCheckout: boolean;
}

const flags: Readonly<FeatureFlags> = {
  newSearch: true,
  redesignedCheckout: false,
};

NonNullable<T>

Strip null and undefined once you’ve validated a value.

type MaybeUser = User | null | undefined;
type SafeUser = NonNullable<MaybeUser>;

ReturnType<T> and Awaited<T>

Infer the result shape from real functions instead of duplicating it by hand.

async function fetchCurrentUser() {
  return { id: '123', name: 'Umesh', role: 'admin' as const };
}

type FetchCurrentUserResult = Awaited<ReturnType<typeof fetchCurrentUser>>;

USE BUILT-INS WHERE THEY HELP READABILITY

Utility types are powerful because they let you derive types from the real source of truth. They are not a license to make every type declaration cryptic.

REACH FOR THEM

Built-ins are the right move when they remove duplication

  • Deriving create/update payloads from domain types
  • Shaping list-item or card-view models from larger objects
  • Extracting async return values from real functions
  • Modeling keyed maps and configuration registries

PULL BACK

Prefer a named custom type when clarity starts dropping

  • Nested utility chains are harder to read than a simple alias
  • Domain concepts deserve names, not just transformations
  • Type cleverness does not replace runtime validation
  • If teammates need to mentally parse the type for 30 seconds, simplify it

Key Takeaways

  • Use Partial for update operations where not all fields are required
  • Use Pick and Omit to create focused types from larger interfaces
  • Use Record for dictionary-like structures
  • Readonly, NonNullable, ReturnType, and Awaited cover a lot of everyday type work
  • Combine utility types for complex transformations
  • These types are zero-cost at runtime — they only exist during compilation

Mastering these utility types will significantly improve your TypeScript code quality and developer experience.

Share this article:
X LinkedIn

Written by Umesh Malik

AI Engineer & Software Developer. Building GenAI applications, LLM-powered products, and scalable systems.