Skip to main content
Juliano Alves
Back to blog

TypeScript utility types I use every day

6 min read
By Juliano Alves

TypeScript’s built-in utility types are not trivia for interviews—they are the default vocabulary for shaping data at boundaries: HTTP payloads, form state, props derived from a larger domain model, and configuration objects.

This post is a practical tour: what I reach for first, how I combine utilities, and when I escalate to mapped or conditional types.

Pick and Omit at HTTP boundaries#

APIs return wide objects. UI and internal services often need narrow projections.

type ApiUser = {
  id: string;
  email: string;
  displayName: string;
  role: 'admin' | 'member';
  mfaEnabled: boolean;
  lastLoginAt: string;
};

/** Public profile card */
type PublicProfile = Pick<ApiUser, 'id' | 'displayName'>;

/** Admin user table row — hide MFA internals from non-admin clients */
type AdminRow = Omit<ApiUser, 'email'>;

Pick and Omit stay readable in code review because the intent is nominal: “same shape as ApiUser but only these keys” or “everything except X”.

Omit and unions#

Omit<T, K> distributes awkwardly when K is a union in some versions—know your TS version. If you need to drop multiple keys and the compiler complains, prefer:

type WithoutSecrets = Pick<ApiUser, Exclude<keyof ApiUser, 'email' | 'mfaEnabled'>>;

Partial, Required, and Readonly#

Partial<T> is the right model for draft entities and multi-step wizards:

type DraftProject = Partial<{
  name: string;
  repoUrl: string;
  defaultBranch: string;
}>;

Before persistence, I often define a separate CreateProjectInput that uses Required on the subset that the backend demands:

type CreateProjectInput = Required<Pick<DraftProject, 'name' | 'repoUrl'>> &
  Partial<Pick<DraftProject, 'defaultBranch'>>;

Readonly<T> shines for configuration objects that should not be mutated after creation:

const routes = {
  home: '/',
  blog: '/blog',
} as const satisfies Readonly<Record<string, `/${string}`>>;

Record for dictionaries#

When keys are a finite union, Record is clearer than index signatures:

type Locale = 'en' | 'pt';

const copy: Record<Locale, { title: string; description: string }> = {
  en: { title: 'Home', description: 'Welcome' },
  pt: { title: 'Início', description: 'Bem-vindo' },
};

If you need partial coverage during migration, Partial<Record<Locale, Messages>> documents that some locales may be missing.

satisfies instead of widening#

Before satisfies, developers often chose between:

  • as const (narrow literals, but awkward when you need to satisfy a wider interface), or
  • explicit annotations (wide types, lost literal precision).
type ThemeColor = 'slate' | 'zinc' | 'stone';

const theme = {
  primary: 'slate',
  secondary: 'zinc',
} satisfies Record<'primary' | 'secondary', ThemeColor>;
// theme.primary is literal 'slate', not ThemeColor

This keeps inference tight while still catching typos against ThemeColor.

Mapped types for DRY transformations#

When every key needs the same transformation, a mapped type beats repeated Pick:

type Nullable<T> = { [K in keyof T]: T[K] | null };

type User = { id: string; name: string };
type NullableUser = Nullable<User>;

Common patterns:

  • Readonly<T> as { readonly [K in keyof T]: T[K] }
  • DeepReadonly<T> for nested structures (recursive mapped type—use sparingly, compile-time cost grows)

Conditional types: use sparingly#

Conditional types (T extends U ? X : Y) are powerful for function overloads and library code. In application code, they often hurt readability.

I reach for them when:

  • Wrapping untyped external data with a discriminated narrowing.
  • Building small type-level helpers (ReturnType, Parameters are built-ins—prefer those first).

Practical decision tree#

  1. Subset or exclusion of keys? → Pick / Omit.
  2. All keys optional or readonly? → Partial / Readonly.
  3. Known key union to values? → Record.
  4. Preserve literals but check assignability? → satisfies.
  5. Same transform on every property? → mapped type.
  6. “If this shape, then that type”? → conditional type (last resort in app code).

Conclusion#

Utility types are compression for intent: they let the compiler enforce invariants at module boundaries without repeating field lists. Mastering Pick, Omit, Partial, Record, and satisfies covers most day-to-day work; escalate to mapped and conditional types when the duplication cost clearly outweighs readability.

© 2026 Juliano Alves. All rights reserved.