Pick, Omit & Record

Difficulty: Advanced

`Pick<T, K>` creates a new type by selecting a subset of properties from `T`. The second type parameter `K` must be a union of keys that exist in `T`. This is useful when you need a view of an object that only includes certain fields - for example, creating a summary type from a detailed entity, or extracting just the fields needed for a specific API call.

`Omit<T, K>` is the complement of `Pick` - it creates a new type by removing specified properties from `T`. If you have a `User` type with 10 properties and you want everything except `password` and `ssn`, `Omit<User, 'password' | 'ssn'>` is much more concise than picking the other 8 fields. Under the hood, `Omit` is defined as `Pick<T, Exclude<keyof T, K>>`.

`Record<K, V>` creates a type with keys of type `K` and values of type `V`. It is the utility type for defining dictionaries and lookup maps. `K` is typically a union of string literals or `string` itself, and `V` is the value type. `Record<string, number>` is equivalent to `{ [key: string]: number }`, but more explicit and composable.

These three utilities are frequently combined in practice. For example, you might use `Pick` to extract relevant fields from a large type, then `Record` to create a mapping from enum values to those picked types. Or use `Omit` to remove sensitive fields before sending data to the client. Mastering these utilities lets you derive precise types from existing ones without duplication.

Code examples

Pick for Creating Subsets

interface User {
  id: number;
  name: string;
  email: string;
  password: string;
  createdAt: string;
}

type UserPreview = Pick<User, "id" | "name" | "email">;
type UserCredentials = Pick<User, "email" | "password">;

function toPreview(user: User): UserPreview {
  return { id: user.id, name: user.name, email: user.email };
}

const user: User = {
  id: 1, name: "Alice", email: "alice@test.com",
  password: "secret", createdAt: "2025-01-01"
};

const preview = toPreview(user);
console.log(`${preview.id}: ${preview.name} (${preview.email})`);

Pick<User, 'id' | 'name' | 'email'> creates a type with only those three properties, useful for API responses that should not expose sensitive fields.

Omit for Removing Fields

interface Product {
  id: string;
  name: string;
  price: number;
  internalCode: string;
  supplierNotes: string;
}

type PublicProduct = Omit<Product, "internalCode" | "supplierNotes">;

function sanitize(product: Product): PublicProduct {
  const { internalCode, supplierNotes, ...public_ } = product;
  return public_;
}

const product: Product = {
  id: "p1", name: "Widget", price: 29.99,
  internalCode: "WDG-001", supplierNotes: "Fragile"
};

const safe = sanitize(product);
console.log(`${safe.name}: ${safe.price}`);

Omit removes the internal fields, creating a safe type for external consumption. Destructuring with rest handles the runtime removal.

Record for Lookup Maps

type Role = "admin" | "editor" | "viewer";

interface Permissions {
  canRead: boolean;
  canWrite: boolean;
  canDelete: boolean;
}

const rolePermissions: Record<Role, Permissions> = {
  admin: { canRead: true, canWrite: true, canDelete: true },
  editor: { canRead: true, canWrite: true, canDelete: false },
  viewer: { canRead: true, canWrite: false, canDelete: false }
};

function describeRole(role: Role): string {
  const perms = rolePermissions[role];
  const abilities = [];
  if (perms.canRead) abilities.push("read");
  if (perms.canWrite) abilities.push("write");
  if (perms.canDelete) abilities.push("delete");
  return `${role}: ${abilities.join(", ")}`;
}

console.log(describeRole("admin"));
console.log(describeRole("viewer"));

Record<Role, Permissions> ensures every role has a permissions entry. If a new role is added to the union, TypeScript will error until its permissions are defined.

Key points

Concepts covered

Pick<T,K>, Omit<T,K>, Record<K,V>, type projection, index signatures