The Utility Types You Actually Need

TypeScript ships with 20+ built-in utility types. Most tutorials list all of them with toy examples. This article covers the nine that appear in real production code every week โ€” with the composition patterns that make them genuinely powerful.

๐Ÿค” Sound familiar?
  • You reach for Partial<T> for update payloads and end up with everything optional including id
  • You've typed the same DTO three times by hand when Pick would have done it in one line
  • You're not sure when to use Extract vs Pickโ€” they sound the same but aren't
  • You use Record<string, T> when you wanted Record<SomeUnion, T> to force exhaustiveness

These nine utility types โ€” and their compositions โ€” eliminate nearly all manual type duplication in a typical TypeScript codebase.

The Core Nine


mindmap
  root((Utility Types))
    Object Shape
      Partial
      Required
      Readonly
      Pick
      Omit
    Union & Keys
      Record
      Extract
      Exclude
    Functions
      ReturnType
      Parameters

1. Partial and Required

interface User {
  id: string;
  name: string;
  email: string;
  avatar: string;
}

// Partial โ€” every property becomes optional
type UpdateUserPayload = Partial<User>;
// { id?: string; name?: string; email?: string; avatar?: string }

// Problem: id shouldn't be updatable
// Solution: Partial on just the fields you want optional
type UpdateUserPayload = Partial<Omit<User, 'id'>> & { id: string };
// { id: string; name?: string; email?: string; avatar?: string }

// Required โ€” reverse: make all optional properties required
interface Config {
  timeout?: number;
  retries?: number;
}

type ResolvedConfig = Required<Config>;
// { timeout: number; retries: number }

// Useful after you've applied defaults
function withDefaults(config: Config): ResolvedConfig {
  return { timeout: 5000, retries: 3, ...config };
}

2. Pick and Omit

Both create a new type from a subset of an object's properties. The difference is perspective: Pick lists what you want,Omitlists what you don't.

interface Article {
  id: string;
  title: string;
  content: string;
  authorId: string;
  publishedAt: Date;
  updatedAt: Date;
  deletedAt: Date | null;
}

// Pick: ideal when you want a small, specific subset
type ArticleCard = Pick<Article, 'id' | 'title' | 'publishedAt'>;
// { id: string; title: string; publishedAt: Date }

// Omit: ideal when you want most of the type minus a few
type CreateArticlePayload = Omit<Article, 'id' | 'publishedAt' | 'updatedAt' | 'deletedAt'>;
// { title: string; content: string; authorId: string }

// Rule of thumb:
// Pick when you want 3 or fewer properties from a large type
// Omit when you want most of the type minus a few specific fields

3. Record

Record<K, V> is not just shorthand for { [key: string]: V }. Its real power is with union key types that enforce exhaustiveness.

type Status = 'pending' | 'processing' | 'completed' | 'failed';

// โŒ Easy to miss a status
const labels: { [key: string]: string } = {
  pending: 'Pending',
  processing: 'Processing',
  // forgot 'completed' and 'failed' โ€” no error
};

// โœ… Record enforces every union member is handled
const labels: Record<Status, string> = {
  pending: 'Pending',
  processing: 'Processing',
  completed: 'Completed',
  // โŒ Error: property 'failed' is missing
  failed: 'Failed',
};

// Also useful for caches and lookup tables
const userCache: Record<string, User> = {};

// With number keys (index signatures)
const httpMessages: Record<number, string> = {
  200: 'OK',
  404: 'Not Found',
  500: 'Internal Server Error',
};

4. Extract and Exclude

These operate on union types โ€” not object types. That's the key difference from Pick and Omit.

type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';

// Extract: keep only members assignable to the second argument
type SafeMethods = Extract<HttpMethod, 'GET' | 'HEAD' | 'OPTIONS'>;
// 'GET' | 'HEAD' | 'OPTIONS'

// Exclude: remove members assignable to the second argument
type MutatingMethods = Exclude<HttpMethod, 'GET' | 'HEAD' | 'OPTIONS'>;
// 'POST' | 'PUT' | 'PATCH' | 'DELETE'

// Practical use: filtering a discriminated union
type Event =
  | { type: 'click'; x: number; y: number }
  | { type: 'keydown'; key: string }
  | { type: 'focus'; element: string }
  | { type: 'blur'; element: string };

type FocusEvents = Extract<Event, { type: 'focus' | 'blur' }>;
// { type: 'focus'; element: string } | { type: 'blur'; element: string }

type MouseEvents = Extract<Event, { type: string; x: number }>;
// { type: 'click'; x: number; y: number }

5. ReturnType and Parameters

async function fetchUser(id: string): Promise<User> {
  const res = await fetch(`/api/users/${id}`);
  return res.json();
}

// ReturnType โ€” extract what a function returns
type FetchUserReturn = ReturnType<typeof fetchUser>;
// Promise<User>

// Awaited + ReturnType โ€” unwrap the Promise too
type ResolvedUser = Awaited<ReturnType<typeof fetchUser>>;
// User

// Parameters โ€” extract the parameter tuple
type FetchUserParams = Parameters<typeof fetchUser>;
// [id: string]

type FirstParam = Parameters<typeof fetchUser>[0];
// string

// Practical use: wrapper functions that preserve the signature
function withLogging<T extends (...args: any[]) => any>(
  fn: T
): (...args: Parameters<T>) => ReturnType<T> {
  return (...args) => {
    console.log('Calling', fn.name, args);
    return fn(...args);
  };
}

Composition Patterns

Deep Partial (for nested updates)

// Built-in Partial only goes one level deep
// For nested structures, define your own:
type DeepPartial<T> = {
  [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};

interface Settings {
  theme: { primary: string; secondary: string };
  notifications: { email: boolean; push: boolean };
}

// With built-in Partial:
type PartialSettings = Partial<Settings>;
// { theme?: { primary: string; secondary: string }; ... }
// โŒ Inner object is NOT partial โ€” you must provide all theme properties

// With DeepPartial:
type DeepPartialSettings = DeepPartial<Settings>;
// { theme?: { primary?: string; secondary?: string }; ... }
// โœ… Can update just one nested field

Pick from a union to get the discriminant

type Action =
  | { type: 'increment'; amount: number }
  | { type: 'decrement'; amount: number }
  | { type: 'reset' };

// Get just the discriminant values as a union
type ActionType = Action['type'];
// 'increment' | 'decrement' | 'reset'

// Map over them for a handler registry
type ActionHandlers = {
  [K in ActionType]: (action: Extract<Action, { type: K }>) => void;
};

Pitfalls

Partial makes IDs optional too

The most common mistake: using Partial<T> for update payloads when id should remain required. Always combine with Omit when fields have different optionality requirements.

Record<string, T> accepts any key

When you have a known set of keys, use a union type instead of string. Record<Status, string> catches missing entries at compile time; Record<string, string>doesn't.

Extract vs Pick confusion

Pick operates on object types (removes properties). Extract operates on union types (filters union members). Using the wrong one produces an error or never โ€” not a subtle type difference.