The Utility Types You Actually Need

An order type existed in four hand-copied versions: the full record, the list item, the create payload and the update payload. A giftMessage field was added to two of them. The list view quietly dropped it, and support could not see gift messages until a customer asked why theirs was missing.

TypeScript ships with more than twenty utility types, and a handful of them cover nearly every real need. Below are the nine I reach for, with the cases where they help and the traps, such as Partial on a whole entity making its id optional.

If you copy interfaces to make variants
  • 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;
};

One Source of Truth for the Order Story

Back to the four copies of the order type from the introduction. The fix is to write the full shape once and derive the rest, so a new field is either picked up everywhere or rejected by the compiler everywhere:

interface Order {
  id: string;
  customerId: string;
  items: OrderItem[];
  giftMessage?: string;
  status: 'pending' | 'paid' | 'shipped';
  createdAt: Date;
}

// Server-owned fields never come from the client
type CreateOrder = Omit<Order, 'id' | 'status' | 'createdAt'>;

// Updates: id is required, a whitelist of fields may change
type UpdateOrder = Pick<Order, 'id'> & Partial<Pick<Order, 'giftMessage' | 'items'>>;

// The list view shows a subset, chosen explicitly
type OrderListItem = Pick<Order, 'id' | 'status' | 'createdAt' | 'giftMessage'>;

Note the difference between Omit and Pick here. CreateOrder uses Omit, so a new field on Order automatically becomes part of the create payload, which is usually what you want for input. OrderListItemuses Pick, so a new field stays out of the list until someone decides to add it. Choose per type based on which default is safer when the base changes, and the gift-message bug becomes a deliberate decision instead of an accident.

Omit on a Union Does Not Do What You Expect

This one catches experienced developers. Omit is built from keyof, and keyof on a union gives you only the keys every member shares. So omitting a field from a discriminated union quietly flattens it:

type Payment =
  | { kind: 'card'; last4: string; auditId: string }
  | { kind: 'bank'; iban: string; auditId: string };

type Bad = Omit<Payment, 'auditId'>;
// { kind: 'card' | 'bank' }   last4 and iban are gone, narrowing no longer works

// Apply Omit to each member instead
type DistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never;

type Good = DistributiveOmit<Payment, 'auditId'>;
// { kind: 'card'; last4: string } | { kind: 'bank'; iban: string }

The T extends unknown ? ... : never wrapper makes the conditional type distribute over each member of the union. Keep aDistributiveOmit in your shared types file; any time you derive from a union, reach for it instead of Omit.

When Derived Types Hurt Readability

Stack enough utilities and hovering over a type showsPick<Omit<Order, "id">, "items"> & Partial<...> instead of the fields. Error messages get the same treatment. A small helper flattens the result for display without changing the type:

type Prettify<T> = { [K in keyof T]: T[K] } & {};

type UpdateOrderView = Prettify<UpdateOrder>;
// hover: { id: string; giftMessage?: string; items?: OrderItem[] }

Derive types internally, where staying in sync matters most. At public boundaries, such as an SDK you publish or an API contract other teams read, consider writing the interface out in full and adding a compile-time check that it still matches the derived version. The explicit interface is what readers see; the check keeps it honest.

Two More Worth Knowing: NoInfer and Your Own

NoInfer<T> (TypeScript 5.4) tells the compiler not to use a particular argument when working out a type parameter. Without it, a wrong value in a later argument widens the inferred type instead of producing an error.

function createState<T extends string>(states: T[], initial: NoInfer<T>) { /* ... */ }

createState(['idle', 'loading'], 'idle');     // ok
createState(['idle', 'loading'], 'done');     // error: 'done' is not 'idle' | 'loading'

When no built-in type fits, compose your own from the same building blocks. Two that most codebases end up writing:

// Make only some keys optional
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

// Flatten an intersection so editor tooltips show the final shape
type Prettify<T> = { [K in keyof T]: T[K] } & {};

type NewUser = Prettify<PartialBy<User, 'id' | 'createdAt'>>;

Keep helper types like these in one shared file with a comment and an example for each. A utility type nobody can find gets written again, slightly differently, in the next feature.

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.