Learn

/

Utility Types

Utility Types

8 patterns

Partial, Required, Pick, Omit, Record, and Extract for transforming types without rewriting them. You'll hit this when you need a version of an existing type with some fields optional or removed.

Avoid
interface User {
  name: string;
  email: string;
  age: number;
}

function updateUser(id: string, data: User) {
  // Caller must pass ALL fields, even
  // if they only want to update one
}

updateUser("1", {
  name: "Alice",
  email: "alice@example.com",
  age: 30,
}); // Just wanted to update name!
interface User {
  name: string;
  email: string;
  age: number;
}

function updateUser(id: string, data: User) {
  // Caller must pass ALL fields, even
  // if they only want to update one
}

updateUser("1", {
  name: "Alice",
  email: "alice@example.com",
  age: 30,
}); // Just wanted to update name!

Prefer
interface User {
  name: string;
  email: string;
  age: number;
}

function updateUser(id: string, data: Partial<User>) {
  // Caller passes only the fields to update
}

updateUser("1", { name: "Alice" }); // OK
interface User {
  name: string;
  email: string;
  age: number;
}

function updateUser(id: string, data: Partial<User>) {
  // Caller passes only the fields to update
}

updateUser("1", { name: "Alice" }); // OK
Why avoid

Requiring the full User object for an update forces callers to re-supply every field, which is tedious and error-prone. If a field is added to User later, every update call breaks. Partial solves this cleanly.

Why prefer

Partial<User> makes every property optional, so callers can pass only the fields they want to update. This is the standard pattern for PATCH-style updates. The type still ensures only valid User properties with correct types are provided.

TypeScript Handbook: Partial
Avoid
interface User {
  id: string;
  name: string;
  email: string;
  passwordHash: string;
  createdAt: Date;
}

// Manually duplicated type
interface UserPreview {
  id: string;
  name: string;
  email: string;
}
interface User {
  id: string;
  name: string;
  email: string;
  passwordHash: string;
  createdAt: Date;
}

// Manually duplicated type
interface UserPreview {
  id: string;
  name: string;
  email: string;
}

Prefer
interface User {
  id: string;
  name: string;
  email: string;
  passwordHash: string;
  createdAt: Date;
}

// Derived from the source of truth
type UserPreview = Pick<User, "id" | "name" | "email">;
interface User {
  id: string;
  name: string;
  email: string;
  passwordHash: string;
  createdAt: Date;
}

// Derived from the source of truth
type UserPreview = Pick<User, "id" | "name" | "email">;
Why avoid

Manually copying properties into a separate interface creates two sources of truth. If name changes from string to { first: string; last: string }, the hand-written UserPreview is silently out of date. Pick prevents this.

Why prefer

Pick<User, "id" | "name" | "email"> creates a new type with only those three properties. If User changes a field's type, the preview type updates automatically. This keeps your types DRY and avoids drift between related shapes.

TypeScript Handbook: Pick
Avoid
interface DbUser {
  id: string;
  name: string;
  email: string;
  passwordHash: string;
}

// Manually rebuilding without passwordHash
interface PublicUser {
  id: string;
  name: string;
  email: string;
}
interface DbUser {
  id: string;
  name: string;
  email: string;
  passwordHash: string;
}

// Manually rebuilding without passwordHash
interface PublicUser {
  id: string;
  name: string;
  email: string;
}

Prefer
interface DbUser {
  id: string;
  name: string;
  email: string;
  passwordHash: string;
}

// Automatically excludes passwordHash
type PublicUser = Omit<DbUser, "passwordHash">;
interface DbUser {
  id: string;
  name: string;
  email: string;
  passwordHash: string;
}

// Automatically excludes passwordHash
type PublicUser = Omit<DbUser, "passwordHash">;
Why avoid

Manually listing all safe fields is fragile. When a new field like avatarUrl is added to DbUser, the manual PublicUser does not include it unless you remember to add it. Omit ensures only the explicitly excluded fields are removed.

Why prefer

Omit<DbUser, "passwordHash"> creates a type with every property except passwordHash. If new fields are added to DbUser, they automatically appear in PublicUser. This is safer than Pick when you want to exclude a small number of fields from a large type.

TypeScript Handbook: Omit
Avoid
// Using a plain object with index signature
const themes: { [key: string]: string } = {
  light: "#ffffff",
  dark: "#1a1a1a",
  ocean: "#0066cc",
};

// Any string key is accepted
themes.nonexistent; // No error, undefined
// Using a plain object with index signature
const themes: { [key: string]: string } = {
  light: "#ffffff",
  dark: "#1a1a1a",
  ocean: "#0066cc",
};

// Any string key is accepted
themes.nonexistent; // No error, undefined

Prefer
type Theme = "light" | "dark" | "ocean";

const themes: Record<Theme, string> = {
  light: "#ffffff",
  dark: "#1a1a1a",
  ocean: "#0066cc",
};

// Only valid keys accepted
themes.light;  // OK
themes.forest; // Error: not in Theme
type Theme = "light" | "dark" | "ocean";

const themes: Record<Theme, string> = {
  light: "#ffffff",
  dark: "#1a1a1a",
  ocean: "#0066cc",
};

// Only valid keys accepted
themes.light;  // OK
themes.forest; // Error: not in Theme
Why avoid

An index signature { [key: string]: string } accepts any string as a key, so TypeScript cannot catch typos or missing entries. It also allows accessing nonexistent keys without errors, returning undefined at runtime.

Why prefer

Record<Theme, string> creates an object type where every key in the Theme union must be present and map to a string. It catches missing keys (if you add a new theme, you must add a color) and rejects invalid keys. Much safer than an index signature.

TypeScript Handbook: Record
Avoid
type User = { name: string } &
  { email: string } &
  { role: "admin" | "user" };

// Hover shows:
// { name: string } & { email: string }
//   & { role: "admin" | "user" }
// Hard to read with many intersections
type User = { name: string } &
  { email: string } &
  { role: "admin" | "user" };

// Hover shows:
// { name: string } & { email: string }
//   & { role: "admin" | "user" }
// Hard to read with many intersections

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

type User = Prettify<
  { name: string } &
  { email: string } &
  { role: "admin" | "user" }
>;

// Hover shows:
// { name: string; email: string;
//   role: "admin" | "user" }
type Prettify<T> = {
  [K in keyof T]: T[K];
} & {};

type User = Prettify<
  { name: string } &
  { email: string } &
  { role: "admin" | "user" }
>;

// Hover shows:
// { name: string; email: string;
//   role: "admin" | "user" }
Why avoid

Raw intersections display as a chain of & in IDE tooltips. With two or three intersections this is manageable, but with more it becomes hard to see what properties are available. The Prettify helper solves this with zero runtime cost since it resolves to the same type.

Why prefer

The Prettify helper uses a mapped type to flatten intersections into a single object type. IDE hover tooltips show a clean, readable object instead of a chain of & intersections. This is especially valuable when composing types from multiple sources like mixins, Pick/Omit combinations, or module augmentations.

Total TypeScript: The Prettify Helper
Avoid
const ROLES = ["admin", "editor", "viewer"] as const;

// Manually duplicating the values as a union
type Role = "admin" | "editor" | "viewer";

// Must update both when roles change
function hasRole(role: Role) { }
const ROLES = ["admin", "editor", "viewer"] as const;

// Manually duplicating the values as a union
type Role = "admin" | "editor" | "viewer";

// Must update both when roles change
function hasRole(role: Role) { }

Prefer
const ROLES = ["admin", "editor", "viewer"] as const;

// Derive the union from the array
type Role = (typeof ROLES)[number];

// "admin" | "editor" | "viewer"
// Auto-updates when ROLES changes
function hasRole(role: Role) { }
const ROLES = ["admin", "editor", "viewer"] as const;

// Derive the union from the array
type Role = (typeof ROLES)[number];

// "admin" | "editor" | "viewer"
// Auto-updates when ROLES changes
function hasRole(role: Role) { }
Why avoid

Manually writing the union duplicates the values. When you add a new role to the array but forget to update the type, the type and runtime array drift apart silently. The indexed access pattern keeps them in sync with zero maintenance.

Why prefer

Indexing a tuple or readonly array with [number] extracts a union of all element types. Combined with as const and typeof, this derives a string literal union directly from a runtime array. Adding or removing an element in ROLES updates the Role type automatically.

Total TypeScript: Indexed Access Types
Avoid
type Event =
  | { type: "click"; x: number; y: number }
  | { type: "keypress"; key: string }
  | { type: "scroll"; offset: number };

// Manually picking event types
type MouseEvent = { type: "click"; x: number; y: number };
type KeyEvent = { type: "keypress"; key: string };
type Event =
  | { type: "click"; x: number; y: number }
  | { type: "keypress"; key: string }
  | { type: "scroll"; offset: number };

// Manually picking event types
type MouseEvent = { type: "click"; x: number; y: number };
type KeyEvent = { type: "keypress"; key: string };

Prefer
type AppEvent =
  | { type: "click"; x: number; y: number }
  | { type: "keypress"; key: string }
  | { type: "scroll"; offset: number };

// Automatically extract matching members
type MouseEvent = Extract<AppEvent, { type: "click" }>;
// { type: "click"; x: number; y: number }

type NonMouseEvent = Exclude<AppEvent, { type: "click" }>;
// keypress | scroll events
type AppEvent =
  | { type: "click"; x: number; y: number }
  | { type: "keypress"; key: string }
  | { type: "scroll"; offset: number };

// Automatically extract matching members
type MouseEvent = Extract<AppEvent, { type: "click" }>;
// { type: "click"; x: number; y: number }

type NonMouseEvent = Exclude<AppEvent, { type: "click" }>;
// keypress | scroll events
Why avoid

Manually duplicating union members creates a maintenance risk. If the click event gains a target property, the hand-written copy is out of date. Extract always reflects the current shape of the source union.

Why prefer

Extract filters a union to members assignable to the given shape. Exclude does the opposite, removing matching members. Both stay in sync when the original union changes. This is cleaner and safer than manually copying type definitions.

TypeScript Handbook: Extract
Avoid
// Manually typing what the function returns
function createUser(name: string, age: number) {
  return { id: crypto.randomUUID(), name, age };
}

// Hand-written, can drift from implementation
type NewUser = { id: string; name: string; age: number };
type CreateArgs = [string, number];
// Manually typing what the function returns
function createUser(name: string, age: number) {
  return { id: crypto.randomUUID(), name, age };
}

// Hand-written, can drift from implementation
type NewUser = { id: string; name: string; age: number };
type CreateArgs = [string, number];

Prefer
function createUser(name: string, age: number) {
  return { id: crypto.randomUUID(), name, age };
}

// Derived from the function itself
type NewUser = ReturnType<typeof createUser>;
// { id: string; name: string; age: number }

type CreateArgs = Parameters<typeof createUser>;
// [name: string, age: number]
function createUser(name: string, age: number) {
  return { id: crypto.randomUUID(), name, age };
}

// Derived from the function itself
type NewUser = ReturnType<typeof createUser>;
// { id: string; name: string; age: number }

type CreateArgs = Parameters<typeof createUser>;
// [name: string, age: number]
Why avoid

Manually writing the return type and parameter types creates a second source of truth. If createUser starts returning an email field, the hand-written NewUser type is silently wrong. ReturnType and Parameters eliminate this class of bugs.

Why prefer

ReturnType extracts the return type and Parameters extracts the parameter tuple from a function type. Both stay in sync with the implementation automatically. This is especially useful when you do not control the function's source or want to avoid exporting an extra type.

TypeScript Handbook: ReturnType