Skip to main content

TS Ep 34: Built-in Utility Types (`Pick` and `Omit`)

Rachmat Hidayat
Author
Rachmat Hidayat
Learn & sharing insights on TypeScript, Go, Kubernetes, DevOps, DevSecOps, SRE, Platform Engineering, AI/ML Engineering, and MLOps.
typescript - This article is part of a series.
Part 34: This Article
Instead of defining separate sub-interfaces for every database query, form input, or API payload, TypeScript’s Pick<T, K> and Omit<T, K> utility types construct derived shapes directly from your master domain models.

1. Pick<T, K> (Selecting Specific Properties)
#

Pick<T, K> constructs a new type by selecting a specific set of keys (K) from a source object type (T).

The second parameter K must extend keyof T.

Production Use Case: Public User Preview DTO
#

When returning user details in a public search endpoint, sensitive fields like passwordHash, twoFactorSecret, and stripeCustomerId must be excluded:

interface DatabaseUser {
  id: string;
  username: string;
  email: string;
  passwordHash: string;
  twoFactorSecret: string;
  stripeCustomerId: string;
  createdAt: Date;
  updatedAt: Date;
}

// 🟢 Pick ONLY 'id', 'username', and 'email' for public API responses:
type PublicUserDto = Pick<DatabaseUser, "id" | "username" | "email">;
/* Inferred Type:
{
  id: string;
  username: string;
  email: string;
}
*/

const publicUser: PublicUserDto = {
  id: "usr_9918",
  username: "alice_dev",
  email: "alice@acme.com",
};

How Pick<T, K> is Defined Under the Hood:
#

type CustomPick<T, K extends keyof T> = {
  [P in K]: T[P];
};

Notice how K extends keyof T enforces that you cannot Pick a property key that does not exist on T:

// ❌ Compiler Error: Type '"nonExistentKey"' is not assignable to type 'keyof DatabaseUser'.
// type BadPick = Pick<DatabaseUser, "id" | "nonExistentKey">;

2. Omit<T, K> (Excluding Specific Properties)
#

Omit<T, K> does the exact opposite of Pick<T, K>. It constructs a type by taking all properties from T and stripping away the specified keys (K).

Production Use Case: Create Entity Payload DTO
#

When a client submits a POST request to create a new database user, the client cannot supply id, createdAt, or updatedAt (because the database auto-generates them upon insertion):

// 🟢 Omit auto-generated database columns and sensitive security credentials:
type CreateUserPayloadDto = Omit<DatabaseUser, "id" | "createdAt" | "updatedAt" | "stripeCustomerId">;
/* Inferred Type:
{
  username: string;
  email: string;
  passwordHash: string;
  twoFactorSecret: string;
}
*/

function createUser(payload: CreateUserPayloadDto): PublicUserDto {
  console.log(`Inserting ${payload.username} into DB...`);
  return {
    id: "usr_new_101",
    username: payload.username,
    email: payload.email,
  };
}

How Omit<T, K> is Defined Under the Hood:
#

Omit combines Pick with the Exclude utility type:

type CustomOmit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

3. Key Constraint Difference: Pick vs Omit
#

A subtle technical difference between Pick and Omit lies in their constraint parameters:

  • Pick<T, K extends keyof T>: K MUST be a valid key of T. If you attempt to pick an invalid key name, tsc throws an error.
  • Omit<T, K extends keyof any>: K is constrained to keyof any (string | number | symbol). Omitting a key name that does not exist on T will NOT throw a compiler error—it simply returns T unchanged.
interface Product {
  sku: string;
  price: number;
}

// ❌ Error: Type '"discount"' is not assignable to 'keyof Product'
// type BadPick = Pick<Product, "sku" | "discount">;

// 🟢 Allowed: 'discount' is ignored because it doesn't exist on Product
type SafeOmit = Omit<Product, "discount">; // Result: { sku: string; price: number }

4. Combining Utility Types (Partial<Omit<T, K>>)
#

In real-world application architectures, utility types are frequently composed together to create complex DTO pipeline types:

// Create DTO for PATCH update requests:
// 1. Omit immutable system fields ('id', 'createdAt')
// 2. Wrap remaining editable fields in Partial<T> so all update keys are optional!
type PatchUserDto = Partial<Omit<DatabaseUser, "id" | "createdAt" | "updatedAt">>;

/* Inferred Type:
{
  username?: string;
  email?: string;
  passwordHash?: string;
  twoFactorSecret?: string;
  stripeCustomerId?: string;
}
*/

5. Decision Matrix: Pick vs Omit
#

ScenarioRecommended ChoiceRationale
Extracting 2 properties from an object with 20 propertiesPickExplicitly lists the small subset of required keys.
Excluding 2 properties from an object with 20 propertiesOmitCleaner than listing 18 keys inside a Pick.
Creating Public API / DTO previewsPickSecure: If new sensitive fields are added to T later, Pick won’t accidentally leak them.
Creating Form Input / Insert DTOsOmitEfficient: Removes auto-generated IDs while keeping all other fields required.
Tip

Security Tip: Use Pick when creating public API DTOs. If an engineer adds a new ssnNumber property to DatabaseUser in the future, Omit might accidentally expose it if the engineer forgets to add ssnNumber to the exclusion array, whereas Pick will safely ignore the new field by default!


Summary & Next Steps
#

In this episode:

  • We analyzed Pick<T, K> and its under-the-hood mapped type definition ([P in K]: T[P]).
  • We analyzed Omit<T, K> and its composition via Pick<T, Exclude<keyof T, K>>.
  • We compared key constraint differences (K extends keyof T vs K extends keyof any).
  • We composed Partial<Omit<T, K>> for update DTOs.
  • We established the Security Rule: Prefer Pick for public DTOs to prevent accidental credential leakage during future schema expansions.

In Episode 35: Utility Types (Record<K, V>), we will explore how to build type-safe dictionary maps and lookup tables using Record!

typescript - This article is part of a series.
Part 34: This Article