Senior 5 min · March 05, 2026
TypeScript Utility Types

TypeScript Omit Union Mistake Leaks Bank Data

Omit<BaseUser,'ssn'> leaked new bankAccount field into API payload.

N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Drawn from code that ran under real load.

Follow
Production
production tested
May 24, 2026
last updated
1,554
articles · all by Naren
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Utility types derive new types from existing ones — no duplication
  • Partial makes all properties optional; Required makes them required
  • Pick selects properties; Omit excludes them
  • Record creates an object type with keys K and values T
  • Conditional types (T extends U ? X : Y) enable type-level logic
  • Senior engineers compose 3-4 utility types to model real API shapes
✦ Definition~90s read
What is TypeScript Utility Types?

Utility types are generic type transformations built into TypeScript that let you derive new types from existing ones without manual redefinition. They exist because real-world TypeScript codebases constantly need variations of the same shapes — partial updates, picked fields, omitted keys, or nullable versions — and writing these by hand is error-prone and unmaintainable.

Imagine you have a detailed employee form with 20 fields — name, age, address, salary, everything.

TypeScript ships roughly 20 built-in utility types like Partial<T>, Pick<T, K>, Omit<T, K>, and Record<K, V>, which collectively save thousands of lines of boilerplate across a typical enterprise application. When you use Omit<User, 'ssn'> to strip a sensitive field, you're relying on a utility type that internally uses mapped types and conditional types to compute the result at compile time.

The mistake in the article's title — using Omit on a union type incorrectly — happens because developers assume Omit distributes over unions like other utility types do, but it doesn't; it operates on the union as a single type, which can silently expose fields you thought were removed. Understanding how utility types compose from mapped and conditional types is essential for avoiding these leaks, especially when dealing with discriminated unions or API response types where a single omitted field on one variant can cascade into a security hole.

In production, you'll often build custom utility types by composing these primitives — for example, DeepPartial<T> or NonNullableFields<T> — and knowing the performance characteristics (e.g., deep conditional types can blow up compilation time on large unions) separates production-grade code from prototypes.

Plain-English First

Imagine you have a detailed employee form with 20 fields — name, age, address, salary, everything. Sometimes you only want to update the address, so you don't want to be forced to fill in all 20 fields again. TypeScript utility types are like a photocopier with special settings: you can say 'give me a copy of this form, but make every field optional' or 'give me a copy with only the name and email fields'. You don't rewrite the form — you transform it. That's exactly what utility types do to your TypeScript interfaces and types.

TypeScript’s type system is powerful, but it’s not magic. Without utility types, you end up writing repetitive type transformations—mapping over object keys, picking properties, or making fields optional—manually for every interface. That’s where utility types come in: they’re built-in type-level functions that do the heavy lifting, so you stop duplicating logic and start catching edge cases at compile time.

What Are Utility Types and Why They Exist

Utility types are generic type transformations that ship with TypeScript. They take one or more existing types and produce a new type based on a specific rule — like making all properties optional (Partial<T>) or picking a subset (Pick<T, K>). These aren't just type-level functions; they're the compiler's way of letting you express shape transformations declaratively.

Before utility types, developers had two choices: duplicate the type definition (fragile) or use any (unsafe). Utility types solve this by making the transformation part of the type system. When you change the source type, every derived type updates automatically — no manual sync, no drift.

The real power isn't in any single utility type. It's in composition. A senior engineer doesn't just use Pick<>, they combine it with Partial<> inside a Record<> to model complex nested API shapes. Understanding how each utility type works internally — especially mapped and conditional types — is what separates safe code from brittle code.

userMapper.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// TheCodeForge — Utility type composition in production
// Define a base type in io.thecodeforge namespace
namespace io.thecodeforge {
  export interface User {
    id: number;
    name: string;
    email: string;
    address: string;
    createdAt: Date;
    isActive: boolean;
  }

  // Partial for PATCH endpoint
  export type UpdateUserBody = Partial<User>;

  // Pick for public profile (no address)
  export type PublicUser = Pick<User, 'id' | 'name' | 'isActive'>;

  // Omit for admin response (hide created date)
  export type AdminUser = Omit<User, 'createdAt'>;

  // Record mapping user IDs to partial updates
  export type BatchUpdates = Record<number, Partial<User>>;
}
Output
// Types are structural — no runtime code emitted.
Think of Utility Types as Type-Level Functions
  • Partial<User> = f(User) → type with all optional properties
  • Pick<User, 'name'|'email'> = g(User, keys) → subtype with only those keys
  • Composition: h(User) = Partial<Pick<User, 'name'|'email'>> → optional subset
  • The type system tracks the derivation — no runtime cost, only compile-time safety
Production Insight
Using utility types eliminates the copy-and-forget bug.
When a new required field is added to a base interface, every Partial<T> or Pick<T, K> automatically adapts — zero manual updates.
The only catch: if you used Omit<T, 'oldField'> and the field is renamed, TypeScript gives no error because Omit with a non-existent key is silently accepted.
Key Takeaway
Utility types are transformations, not modifications.
They derive new types without mutating the original.
This is the declarative type safety that makes refactoring fearless.
TypeScript Utility Types: Omit Mistake Leaks Data THECODEFORGE.IO TypeScript Utility Types: Omit Mistake Leaks Data Flow from utility types to Omit misuse exposing bank data Utility Types & Mapped Types Building blocks: Pick, Omit, Record Conditional Types & Extract/Exclude Type-level filtering with Exclude Omit on Union Types Omit removes keys from each union member Leaked Bank Data Omit on union exposes unintended fields Custom Utility Type Fix Use DistributiveOmit to safely exclude ⚠ Omit on union types can leak sensitive fields Use DistributiveOmit = T extends any ? Omit : never THECODEFORGE.IO
thecodeforge.io
TypeScript Utility Types: Omit Mistake Leaks Data
Typescript Utility Types

Mapped Types — The Building Blocks of Utility Types

Mapped types are the engine behind most utility types. A mapped type iterates over the keys of an existing type and applies a transformation to each property. The syntax is { [P in K]: T } where K is a union of keys (usually keyof T) and T is the property type (often T[P]).

The built-in utility types Partial, Required, Readonly, and Pick are all implemented as mapped types. Understanding the mapped type pattern lets you write your own variations — like Nullable<T> (add | null to each property) or ReadonlyDeep<T> (recursive readonly).

TypeScript 5.0 introduced key remapping via as clause in mapped types: { [K in keyof T as NewKey]: T[K] }. This enables renaming keys during transformation — crucial for API adapters where the external field names differ from internal models.

mappedTypes.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// TheCodeForge — Custom mapped type that adds null to every property
type Nullable<T> = {
  [K in keyof T]: T[K] | null;
};

// Example with io.thecodeforge.Profile
namespace io.thecodeforge {
  export interface Profile {
    name: string;
    age: number;
    email: string;
  }

  export type NullableProfile = Nullable<Profile>;
  // Result: { name: string | null; age: number | null; email: string | null; }

  // Key remapping: prefix every key with 'api_'
  export type Prefixed<T> = {
    [K in keyof T as `api_${string & K}`]: T[K];
  };

  // Also useful for filtering keys by value type
  export type StringKeys<T> = {
    [K in keyof T as T[K] extends string ? K : never]: T[K];
  };
}
Output
// NullableProfile: all properties become string | null or number | null
Key Remapping Power
The as clause in mapped types (TypeScript 4.1+) lets you filter, rename, or even exclude keys during mapping. Use as never to remove keys based on conditions.
Production Insight
Mapped types are zero-cost abstractions — they resolve at compile time.
But deeply nested mapped types (e.g., recursive deep partial) can trigger 'Type instantiation excessively deep' errors in older TypeScript versions.
Limit recursion depth explicitly with a numeric parameter to keep the compiler performant.
Key Takeaway
Mapped types give you control over every property individually.
The as clause turns them into a full type-level query language.
If you can loop over keys, you can transform any shape.

Conditional Types — Type-Level Logic

Conditional types introduce branching logic at the type level. The syntax T extends U ? X : Y checks if T is assignable to U. If yes, resolves to X; otherwise Y. This is the type system's if statement.

Conditional types are the foundation for Exclude<T, U>, Extract<T, U>, NonNullable<T>, and ReturnType<T>. They also power advanced patterns like Flatten<T> (unbox promises) or DeepPromise<T>.

A critical nuance: conditional types distribute over unions when the checked type T is a bare type parameter. For example, type IsString<T> = T extends string ? true : false; when called with string | number returns true | false (distributes). To prevent distribution, wrap in square brackets: [T] extends [string] ? true : false. This is the single most common source of bugs in production conditional types.

conditionalTypes.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// TheCodeForge — Conditional type for extracting arrays
namespace io.thecodeforge {
  export type ExtractArray<T> = T extends (infer U)[] ? U : never;

  // Example
  type Items = ExtractArray<string[]>; // string
  type NotArray = ExtractArray<number>; // never

  // Nested conditional with infer
  export type UnboxPromise<T> = T extends Promise<infer U> ? U : T;

  // Distribution example: avoid by wrapping
  export type IsString<T> = [T] extends [string] ? true : false;
  type Test = IsString<string | number>; // false (distribution prevented)

  // Production pattern: conditional + mapped
  export type NonNullableProperties<T> = {
    [K in keyof T as T[K] extends null | undefined ? never : K]: T[K];
  };
}
Output
// ExtractArray<string[]> yields string
Distribution Trap
Conditional types automatically distribute over unions. To prevent distribution and check the whole union at once, wrap both sides in square brackets: [T] extends [U].
Production Insight
The distribution behavior of conditional types is the #1 cause of unexpected 'never' in production type assertions.
When you see 'Property does not exist on type never', the fix is almost always adding brackets to stop distribution.
Never write a conditional type without thinking: 'Do I want distribution here or not?'
Key Takeaway
Conditional types bring if-else logic to the type level.
Distribution is default — wrap in [] to check the union as a single type.
Master this and you unlock recursive type transformations.

Building Custom Utility Types — Composition Patterns

The built-in utility types cover 80% of use cases. The remaining 20% require composing them or building bespoke utilities using mapped and conditional types. Custom utility types are how senior engineers model complex domain constraints — like a type that represents a validatable form state (all fields optional + original for reference).

A common pattern is DeepPartial<T> — recursively makes all nested properties optional. It uses mapped types with conditional types to handle primitives, arrays, and objects differently. Another is PickNullableKeys<T> which extracts only keys whose values are nullable — useful for generating a type-safe update payload for partial database columns.

Custom utility types should follow the same conventions as built-in ones: generic, documented, and composed from smaller transformations. Always annotate the constraint on the generic parameter (e.g., T extends object) to give better error messages when misused.

customUtilities.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// TheCodeForge — Combining mapped and conditional types
namespace io.thecodeforge {
  // DeepPartial with max depth guard
  export type DeepPartial<T, Depth extends number = 3> = {
    [K in keyof T]?: T[K] extends object
      ? Depth extends 0
        ? T[K]
        : DeepPartial<T[K], Prev[Depth]>
      : T[K];
  };

  // Helper: prev array decreases depth counter
  type Prev = [never, 0, 1, 2, 3, 4, 5];

  // Pick only keys whose values are nullable
  export type NullableKeys<T> = {
    [K in keyof T]-?: T[K] extends null | undefined ? K : never;
  }[keyof T];

  export type PickNullable<T> = Pick<T, NullableKeys<T>>;

  // Use case: API response with both value and original
  export type EditableField<T, K extends keyof T> = {
    value: T[K];
    original: T[K];
  };
}
Output
// DeepPartial<User> makes all nested properties optional up to depth 3
Compose Utility Types Like Functions
  • Start with base type → Pick → Partial → end with Record
  • Add conditional branches for optional fields
  • Wrap in a generic with constraints for reusability
  • Test edge cases: empty objects, unions, deep nesting
Production Insight
Custom utility types without depth limits are the leading cause of 'Type instantiation excessively deep' errors in large codebases.
Always add a numeric depth parameter (default 3-5) to recursive utility types.
When a junior dev tries to use DeepPartial on a deeply nested GraphQL response, the compiler will hang without this guard.
Key Takeaway
Build custom utilities by composing Partials, Picks, and conditionals.
Always guard recursion depth — the compiler can't protect itself.
Document the transformation pipeline in a JSDoc comment for team readability.

Performance, Debugging and Production Best Practices

Utility types are zero-cost abstractions — they disappear at runtime. But their compile-time cost can be significant. Deeply nested mapped types, especially recursive ones, can trigger exponential type instantiation. TypeScript 5.5 improved performance with structural caching, but large codebases (500k+ lines) still hit limits.

Production debugging of utility type errors requires understanding two mechanisms: distribution (already covered) and the infer keyword in conditional types. infer allows extracting a type from a conditional and is used by ReturnType<T> and Parameters<T>. A common mistake is nesting infer inside an extends clause incorrectly — it only works in the true branch of a conditional.

Best practices: (1) always specify generic constraints (T extends object), (2) avoid deep recursion (>10 levels), (3) prefer composition of built-in utilities over writing new ones, (4) use typeof and keyof to dynamically derive key unions instead of hardcoding them.

productionPatterns.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// TheCodeForge — Production best practices
namespace io.thecodeforge {
  // 1. Constrain generics for better error messages
  export function updateUser<T extends Partial<User>>(id: number, changes: T): void {}

  // 2. Prefer composition over custom
  export type CreateUserRequest = Readonly<Omit<User, 'id' | 'createdAt'>>;

  // 3. Use keyof to avoid hardcoded keys
  export type EditableKeys = 'name' | 'email';
  export type EditableUser = Pick<User, EditableKeys>;

  // 4. Use infer carefully in return type extraction
  export type GetReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
}
Output
// Composition of built-ins often solves the problem without custom types
TypeScript's Structural Caching (5.5+)
TypeScript now caches structural type comparisons, drastically reducing compile times for repeated utility type evaluations. Leverage it by keeping type derivations shallow.
Production Insight
A project with 300+ custom utility types and no depth limits can increase compile time from 5 seconds to 45 seconds.
The first thing I do on a slow CI is grep for 'DeepPartial' and 'DeepRequired' and add depth parameters.
Combine with incremental builds (tsc --incremental) to get the worst hit on first compile only.
Key Takeaway
Utility types are free at runtime but cost at compile time.
Recursion depth is the hidden performance killer — always cap it.
Prefer composition of built-in utilities over deep custom nesting.

Pick and Omit: Slicing Interfaces Without Surgery

Stop importing giant interfaces when you only need two fields. Pick<T, K> lets you extract specific keys from a type. Omit<T, K> does the inverse—it removes keys. These are not just for cleaning up; they prevent you from coupling components to bloated data shapes. If your backend returns a sprawling User object but your UI only needs email and role, Pick<User, 'email' | 'role'> keeps your frontend honest. Omit is crucial for DTOs: strip sensitive fields like passwordHash before sending data to the client. Both utilities work because they rely on mapped types under the hood—they iterate over keys, not values. This means you get full IDE autocompletion and type safety, not loose objects. Never spread a full entity into a component again. Slice it first.

userTransformer.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface User {
  id: string;
  email: string;
  role: 'admin' | 'user';
  passwordHash: string;
  createdAt: Date;
}

type PublicUser = Omit<User, 'passwordHash'>;
type UserContact = Pick<User, 'email' | 'role'>;

function sendWelcomeEmail(contact: UserContact): string {
  return `Emailing ${contact.email} with role ${contact.role}`;
}

const admin: PublicUser = { id: '1', email: 'admin@x.com', role: 'admin', createdAt: new Date() };
console.log(sendWelcomeEmail(admin));
Output
"Emailing admin@x.com with role admin"
Production Trap:
Pick and Omit do not guard against runtime data. If you Pick a field that’s missing in the actual API response, TypeScript won’t yell at runtime. Always validate external data with a runtime parser like Zod or io-ts before wrapping it in a Pick type.
Key Takeaway
Your contract should match your consumer. Pick what you need, Omit what you don't.

Record: The One Type to Rule Them All for Dictionaries

Stop typing objects with [key: string]: any. Record<K, V> creates a typed dictionary where keys are from K and values are V. It is the fastest way to model lookup tables, config maps, or enum-to-description mappings. When K is a union of string literals, Record locks the allowed keys at compile time. No arbitrary string keys slipping through. When K is string, you get a flexible map with uniform value types. Combine Record with Partial or Required to fine-tune optionality. This utility dominates real-world code because it replaces verbose index signatures with a single, readable type. If you find yourself writing { [id: string]: number }, stop. Use Record<string, number>. Your future self will thank you.

rolePermissions.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
type Role = 'admin' | 'editor' | 'viewer';
type Permissions = 'read' | 'write' | 'delete';

const rolePermissions: Record<Role, Permissions[]> = {
  admin: ['read', 'write', 'delete'],
  editor: ['read', 'write'],
  viewer: ['read'],
};

function canDelete(role: Role): boolean {
  return rolePermissions[role].includes('delete');
}

console.log(canDelete('editor'));
Output
false
Pro Tip:
When using Record with a large union of keys, TypeScript's structural typing works in your favor. But if you need every key from the union to exist, combine it with Required<Record<K, V>>. This forces you to handle every case—no accidental omissions.
Key Takeaway
Record<K, V> is the type-safe hammer for every dictionary nail in your codebase.

Extract and Exclude: Type-Level Filtering That Saves You Runtimes

Conditional types are powerful, but Extract<T, U> and Exclude<T, U> give you instant set operations on unions. Extract returns the subset of T that is assignable to U. Exclude removes it. These are invaluable for filtering discriminated unions or whitelisting specific literal types. Instead of writing complex conditional types by hand, reach for these built-ins. They compile to plain types—zero runtime overhead. Use Exclude to strip error states from a union, or Extract to grab only success types. Pattern: Exclude<'a' | 'b' | 'c', 'a'> yields 'b' | 'c'. It's set theory for types. Mastering these two utilities lets you build type-safe state machines, reducer actions, and API response handlers without a single conditional type definition.

apiResponses.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type ApiResponse = 
  | { status: 'success'; data: string }
  | { status: 'error'; message: string }
  | { status: 'loading' };

// Only success responses
type SuccessResponse = Extract<ApiResponse, { status: 'success' }>;
// Drop the error case
type NonErrorResponse = Exclude<ApiResponse, { status: 'error' }>;

function handleSuccess(res: SuccessResponse): string {
  return res.data;
}

const example: SuccessResponse = { status: 'success', data: 'hello' };
console.log(handleSuccess(example));
Output
"hello"
Production Trap:
Exclude and Extract work on unions, not intersections. If you pass an intersection type like A & B, results may surprise you. Always flatten to a union first with a distributive conditional type or use a union directly.
Key Takeaway
Filter your types the same way you filter arrays. Extract what you need, Exclude what you don't.
● Production incidentPOST-MORTEMseverity: high

The Silent API Type Drift That Cost a Sprint

Symptom
A new 'bankAccount' field was added to the database schema, but the API response still used Omit<BaseUser, 'ssn'>. The field leaked into the response payload, exposing banking details to all authenticated users.
Assumption
The team assumed Omit<BaseUser, 'ssn'> would always hide all sensitive fields because 'ssn' was the original one. They didn't realise Omit only removes the keys you explicitly pass — it doesn't 'protect' against new fields.
Root cause
No explicit list of omitted keys was maintained; Omit was used as a one-off transformation rather than a composition of utility types. A custom type like SafeApiResponse<T> using Omit with a union of sensitive keys would have caught the new field.
Fix
Replace the ad-hoc Omit with a dedicated type: type SafeApiUser = Omit<BaseUser, 'ssn' | 'bankAccount'>. Then add a lint rule forbidding direct Omit without a documented key union.
Key lesson
  • Never use Omit with a single key in production — always use a union of all keys you intend to exclude
  • Consider creating a utility type like PublicUser<T> that explicitly lists omitted keys in one place
  • When adding a new sensitive field to a base type, update every utility type that derives from it
Production debug guideHow to diagnose the most common compile-time failures with utility types4 entries
Symptom · 01
Type instantiation is excessively deep and possibly infinite
Fix
Check for recursive mapped types (e.g., DeepPartial<T> that recurses into arrays). Use TypeScript 5.5's --diagnostics or limit recursion depth with a max level parameter.
Symptom · 02
Type 'X' does not satisfy the constraint 'keyof T'
Fix
Verify the key union passed to Pick or Omit actually exists on the source type. Use keyof T to generate a dynamic union instead of hardcoding string literals.
Symptom · 03
Conditional type evaluates to 'never' unexpectedly
Fix
Check if the type distribution is happening (e.g., T extends U ? ... where T is a union). Add square brackets to prevent distribution: [T] extends [U] ? ...
Symptom · 04
Mapped type modifies read-only properties incorrectly
Fix
Use -readonly modifier in mapped type to strip readonly, or use the built-in Mutable<T> if needed. Verify the mapping includes all property modifiers.
★ Utility Type Error Quick-Fix CardOne-liner fixes for the three most annoying utility type errors in production code.
Type instantiation excessively deep (error 2589)
Immediate action
Add a depth limit — replace DeepPartial<T> with DeepPartial<T, 5> using a numeric parameter
Commands
"${"type DeepPartial<T, Depth extends number = 3> = Depth extends 0 ? T : { [K in keyof T]?: DeepPartial<T[K], Prev[Depth]> } }
npx tsc --diagnostics --generateTrace traceOutput && analyze-trace
Fix now
For quick relief: change recursive conditional to a simpler flat type or increase recursion limit in tsconfig (not recommended for production).
Property does not exist on type 'never' derived from conditional+
Immediate action
Check if the conditional type is distributing over a union — add brackets: [T] extends [U]
Commands
// Instead of: type Foo<T> = T extends Bar ? ... do: type Foo<T> = [T] extends [Bar] ? ...
console.log the union members using a custom Debug<T> type: type Debug<T> = { [K in keyof T]: T[K] }
Fix now
Replace T extends U with [T] extends [U] everywhere — stops distribution and makes conditional work on full union.
Type 'readonly string[]' is not assignable to type 'string[]' after mapped type+
Immediate action
Strip readonly modifier in the mapped type using -readonly
Commands
type Mutable<T> = { -readonly [K in keyof T]: T[K] }
Use Readonly<Mutable<Original>> to toggle back and forth
Fix now
Apply Mutable<T> before passing to function that expects writable array.
Common Utility Types at a Glance
Utility TypeUse CaseExample
Partial<T>API PATCH body — all fields optionaltype UpdateUser = Partial<User>
Required<T>After optional fields validation passestype VerifiedUser = Required<PartialUser>
Pick<T, K>Select subset for public viewtype PublicUser = Pick<User, 'id' | 'name'>
Omit<T, K>Exclude sensitive fieldstype SafeUser = Omit<User, 'ssn' | 'bankAccount'>
Readonly<T>Immutable config or statetype AppConfig = Readonly<Config>
Record<K, T>Map from keys to uniform valuestype UserMap = Record<number, User>
Exclude<T, U>Remove union memberstype WithoutString = Exclude<string | number, string>
Extract<T, U>Extract union memberstype OnlyString = Extract<string | number, string>
ReturnType<T>Get function return typetype ApiResponse = ReturnType<typeof fetchUsers>
Custom: DeepPartial<T>Nested partial update for GraphQL or MongoDBtype NestedUpdate = DeepPartial<User, 5>

Key takeaways

1
Utility types derive new types from existing ones declaratively
no manual duplication, no drift.
2
Mapped types (Partial, Pick, Record) iterate over keys; conditional types (Exclude, ReturnType) add branching logic.
3
Composition
combine Pick, Partial, and Record to model complex API shapes in one line.
4
Distribution is the #1 gotcha with conditional types
wrap in [] to control it.
5
Recursive utility types must always have a depth limit to avoid compiler hangs in production codebases.

Common mistakes to avoid

3 patterns
×

Using Omit with a single key and forgetting to update when new sensitive fields are added

Symptom
New fields leak into API responses because Omit only removes the keys you specify — it's not a whitelist.
Fix
Always use a union of all keys to exclude: Omit<T, 'field1' | 'field2'>. Even better, create a dedicated type like PublicUser<T> that aggregates all omitted keys in one place with a comment.
×

Not preventing distribution in conditional types, leading to unexpected 'never'

Symptom
A conditional type like T extends string ? 'yes' : 'no' when T is string | number evaluates to 'yes' | 'no' instead of 'no' — distribution happens.
Fix
Wrap the checked type in brackets: [T] extends [string] ? 'yes' : 'no'. This treats the union as a single type and prevents distribution.
×

Building recursive mapped types without a depth limit, causing 'Type instantiation excessively deep' errors

Symptom
Compiler hangs or throws error 2589 when the type is instantiated with a deeply nested object.
Fix
Add a numeric generic parameter (e.g., Depth extends number = 3) and decrement it at each recursion level. Use a helper type like Prev to count down.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Explain the difference between Partial and Pick. When would you...
Q02JUNIOR
What is a mapped type and how does it relate to utility types like Reado...
Q03SENIOR
Explain conditional type distribution and how to control it. Provide a r...
Q01 of 03SENIOR

Explain the difference between Partial and Pick. When would you use each in a REST API design?

ANSWER
Partial<T> makes all properties of T optional — perfect for PATCH endpoints where you only send the fields that changed. Pick<T, K> selects a subset of keys — ideal for GET responses where you want to expose only specific fields (e.g., a public profile without sensitive data). In a REST API, you might combine them: Partial<Pick<User, 'name' | 'email'>> for a PATCH that only allows updating name and email.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
Are utility types available at runtime?
02
Can I use utility types with union types?
03
What's the difference between Pick and Omit?
04
How do I create a type that makes all properties nullable?
05
Why does `ReturnType` not work with generic functions?
N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Drawn from code that ran under real load.

Follow
Verified
production tested
May 24, 2026
last updated
1,554
articles · all by Naren
🔥

That's TypeScript. Mark it forged?

5 min read · try the examples if you haven't

Previous
TypeScript Enums and Decorators
7 / 15 · TypeScript
Next
TypeScript Utility Types Deep Dive: Real Examples from Production