| |

TypeScript 35 ๐Ÿ”ท Utility Types โ€” Record, Exclude, Extract, NonNullable

Chapter 34 covered the object-shape utilities: Partial, Required, Readonly, Pick, Omit. This chapter covers the union utilities and the one object-construction utility: Record<K, V> builds an object type from a key type and a value type, Exclude<T, U> removes union members assignable to U, Extract<T, U> keeps only the members assignable to U, and NonNullable<T> removes null and undefined. Each is built on conditional types and mapped types โ€” the same primitives from earlier chapters. Together they complete the core utility vocabulary you’ll use every day.

Key point: Record<K, V> is a mapped type over a union of keys. Exclude, Extract, and NonNullable are conditional types that distribute over unions. Exclude and Extract are exact complements โ€” Exclude removes what matches, Extract keeps what matches. NonNullable is a specialized case of Exclude. Understanding one means understanding the others: they’re all the same pattern applied to different problems.


Record<K, V> โ€” build an object type

Record<K, V> creates an object type with keys K and values V.

type Scores = Record<string, number>;

const scores: Scores = {
  alice: 10,
  bob: 20
};

scores.charlie = 30;      // โœ… any string key
scores.dave = 'ten';      // โŒ value must be number

Every key is a string; every value is a number.

Literal union keys:

type Status = 'idle' | 'loading' | 'ready' | 'error';
type StatusMessages = Record<Status, string>;

const messages: StatusMessages = {
  idle: 'Waiting',
  loading: 'Loading...',
  ready: 'Ready',
  error: 'Failed'
};
// Must have all four โ€” no more, no less

This is the exhaustive-map pattern. Every status must have a message; adding a new status breaks the object until updated.

Implementation:

type Record<K extends keyof any, V> = {
  [P in K]: V;
};

Iterates over K and produces each key with value type V.

K extends keyof any: keyof any is string | number | symbol โ€” the set of all possible key types. The constraint keeps K to keys.

Why Record is useful:

  • Lookup tables โ€” maps from a known set of keys
  • Exhaustive maps โ€” every case must be present
  • Configuration objects โ€” keyed by a union of names
  • Dynamic dictionaries โ€” Record<string, V> for arbitrary keys

Record with literal values:

type Endpoints = Record<'users' | 'posts' | 'comments', string>;

const endpoints: Endpoints = {
  users: '/api/users',
  posts: '/api/posts',
  comments: '/api/comments'
};

Record vs index signature:

// These are equivalent
type A = Record<string, number>;
type B = { [key: string]: number };

Record is the mapped-type form; the index signature is the direct form. Both produce the same type.

Partial Record โ€” not all keys required:

type PartialMessages = Partial<Record<Status, string>>;
// { idle?: string; loading?: string; ... }

Combining with Partial makes every entry optional.

Readonly Record:

type FrozenMessages = Readonly<Record<Status, string>>;

Why Record matters: It’s how you type maps. A union of keys plus a value type gives you an object with the exact shape you want โ€” no more, no less. Libraries use it for routes, config, handlers, and any keyed collection.

Why Record is a mapped type: It has the shape { [P in K]: V } โ€” iterate K, produce each key. That’s exactly what a mapped type does. Record<K, V> is the named, reusable form of the same pattern you’d write by hand.


Exclude<T, U> โ€” remove union members

Exclude<T, U> removes from T any member assignable to U.

type A = Exclude<'a' | 'b' | 'c', 'b'>;
// 'a' | 'c'

type B = Exclude<string | number | boolean, string>;
// number | boolean

type C = Exclude<'a' | 'b', 'a' | 'b'>;
// never

Each member of T is checked against U. Members assignable to U become never (removed from the union); the rest remain.

Implementation:

type Exclude<T, U> = T extends U ? never : T;

A conditional type. Because T is a naked type parameter, it distributes over unions โ€” each member is checked separately.

Distribution in action:

// Distributes:
// 'a' extends 'b' ? never : 'a'  โ†’ 'a'
// 'b' extends 'b' ? never : 'b'  โ†’ never
// 'c' extends 'b' ? never : 'c'  โ†’ 'c'
// combine: 'a' | 'c'

never disappears from unions: 'a' | never | 'c' collapses to 'a' | 'c'. That’s how removal works โ€” the matching members become never, which has no effect on the union.

Common uses:

// Remove specific literals
type Status = 'idle' | 'loading' | 'ready' | 'error';
type ActiveStatus = Exclude<Status, 'idle'>;
// 'loading' | 'ready' | 'error'

// Remove a type from a union
type NotString = Exclude<string | number | boolean, string>;
// number | boolean

// Remove null and undefined (same as NonNullable)
type NonNull = Exclude<string | null | undefined, null | undefined>;
// string

Exclude with generic unions:

type ArrayOnly<T> = Extract<T, unknown[]>;
type NotArray<T> = Exclude<T, unknown[]>;

type A = NotArray<string | number[] | boolean>;
// string | boolean

Distribution filters the union by shape.

Why Exclude matters: It’s how you subtract from a union. Often a type is a union of options, and you want to remove some. Exclude does that at the type level. It’s used in Omit internally โ€” Omit<T, K> = Pick<T, Exclude<keyof T, K>>.

What Exclude doesn’t do: It doesn’t remove arbitrary subtypes structurally. It checks assignability. Exclude<{ a: 1 } | { a: 1; b: 2 }, { a: 1 }> removes both, because both are assignable to { a: 1 }. If you want to remove exact matches only, you need a different comparison.

Why distribution is essential: Without it, Exclude<'a' | 'b' | 'c', 'b'> would treat the whole union as one type and check if it’s assignable to 'b' โ€” which it isn’t, so nothing would be removed. Distribution splits the union first, checks each member, and combines results. That’s the whole mechanism.


Extract<T, U> โ€” keep union members

Extract<T, U> keeps only the members of T assignable to U. It’s the exact complement of Exclude.

type A = Extract<'a' | 'b' | 'c', 'a' | 'b'>;
// 'a' | 'b'

type B = Extract<string | number | boolean, string>;
// string

type C = Extract<'a' | 'b', 'a' | 'b'>;
// 'a' | 'b'

Members assignable to U are kept; the rest become never.

Implementation:

type Extract<T, U> = T extends U ? T : never;

The conditional is inverted from Exclude โ€” return T on match instead of never.

Distribution:

// Distributes:
// 'a' extends 'a' | 'b' ? 'a' : never  โ†’ 'a'
// 'b' extends 'a' | 'b' ? 'b' : never  โ†’ 'b'
// 'c' extends 'a' | 'b' ? 'c' : never  โ†’ never
// combine: 'a' | 'b'

Common uses:

// Keep only specific literals
type Status = 'idle' | 'loading' | 'ready' | 'error';
type FinalStatus = Extract<Status, 'ready' | 'error'>;
// 'ready' | 'error'

// Filter by shape
type Arr = Extract<string | number[] | boolean | string[], unknown[]>;
// number[] | string[]

// Extract functions from a union
type Fns = Extract<string | (() => void) | number, Function>;
// () => void

Filtering by prefix with template literals:

type Events = 'userCreated' | 'userDeleted' | 'orderPlaced';
type UserEvents = Extract<Events, `user${string}`>;
// 'userCreated' | 'userDeleted'

Combining Extract with template literal types filters unions by string pattern. That’s a very common pattern.

Why Extract matters: It’s how you select from a union. When you have a union of many options and want only some, Extract picks them out. Combined with template literal types, it filters by name pattern. Combined with shape checks (unknown[], Function), it filters by structure.

Extract and Exclude are duals: Every union member is in exactly one of the two results.

type T = 'a' | 'b' | 'c' | 'd';
type U = 'b' | 'd';

type Kept = Extract<T, U>;    // 'b' | 'd'
type Removed = Exclude<T, U>; // 'a' | 'c'

// Kept | Removed = T

Why Extract is more specific than Exclude: Exclude removes what matches; Extract keeps what matches. They’re complements, but naming matters. When the set you’re filtering by is small, Extract reads better. When the exception set is small, Exclude does. Pick the one that matches your intent.


NonNullable<T> โ€” remove null and undefined

NonNullable<T> removes null and undefined from T.

type A = NonNullable<string | null>;
// string

type B = NonNullable<string | null | undefined>;
// string

type C = NonNullable<number | undefined>;
// number

type D = NonNullable<string>;
// string (no change)

Distribution removes the null and undefined branches; the rest remains.

Implementation:

type NonNullable<T> = T extends null | undefined ? never : T;

A specialized Exclude. It’s equivalent to:

type NonNullable<T> = Exclude<T, null | undefined>;

Distribution:

// string extends null | undefined ? never : string โ†’ string
// null extends null | undefined ? never : null โ†’ never
// combine: string | never = string

On objects with optional properties: NonNullable<T> doesn’t remove optionality from properties.

interface User {
  id: number;
  nickname?: string;
}

type A = NonNullable<User>;
// User (no change โ€” the type itself isn't null)
// nickname is still optional

NonNullable<User> removes null from the type User, but User isn’t nullable. To remove undefined from nickname, you’d apply NonNullable to the property:

type Nickname = NonNullable<User['nickname']>;
// string

On function returns:

function findUser(id: number): User | null {
  // ...
}

type FoundUser = NonNullable<ReturnType<typeof findUser>>;
// User

Useful for typing the result of a nullable function after checking.

On optional values:

function process(value?: string) {
  const v = value as NonNullable<typeof value>;
  // v: string
}

After a null check, you can narrow the type.

Why NonNullable matters: Null and undefined are the most common sources of runtime errors. NonNullable<T> strips them, expressing “we know this isn’t null now.” It’s used after checks, in generic utilities, and when transforming nullable types.

What it doesn’t do: It doesn’t check at runtime. It’s a type-level transformation โ€” the compiler removes the possibility, but the runtime value could still be null if you lied about it.

Why NonNullable is useful even though it’s a special case of Exclude: The name says what it does. Exclude<T, null | undefined> is correct but noisy. NonNullable<T> reads as the intent. Named utilities are more expressive than their definitions โ€” that’s why they exist.


Combining the utilities

These four work together in real patterns.

Exhaustive lookup table:

type Status = 'idle' | 'loading' | 'ready' | 'error';
type StatusConfig = Record<Status, { color: string; icon: string }>;

const config: StatusConfig = {
  idle: { color: 'gray', icon: 'โ—‹' },
  loading: { color: 'blue', icon: 'โ—' },
  ready: { color: 'green', icon: 'โ—' },
  error: { color: 'red', icon: 'โœ•' }
};

Adding a status to the union fails to compile until config is updated.

Filter events by prefix:

type AllEvents =
  | 'userCreated'
  | 'userUpdated'
  | 'userDeleted'
  | 'orderPlaced'
  | 'orderShipped'
  | 'systemReady';

type UserEvents = Extract<AllEvents, `user${string}`>;
// 'userCreated' | 'userUpdated' | 'userDeleted'

type NonUserEvents = Exclude<AllEvents, `user${string}`>;
// 'orderPlaced' | 'orderShipped' | 'systemReady'

Remove one option from a config union:

type LogLevel = 'debug' | 'info' | 'warn' | 'error';
type VisibleLevel = Exclude<LogLevel, 'debug'>;
// 'info' | 'warn' | 'error'

type LevelConfig = Record<VisibleLevel, boolean>;
// Must have all three

Nullable values:

type Maybe<T> = T | null | undefined;

function unwrap<T>(value: Maybe<T>): NonNullable<T> {
  if (value === null || value === undefined) {
    throw new Error('Value is nullish');
  }
  return value;
}

const a = unwrap<string>('hello');  // string
// unwrap<string>(null);  // throws at runtime

Filter records by value type:

type OnlyStrings<T> = {
  [K in keyof T as T[K] extends string ? K : never]: T[K];
};

type Mixed = Record<'a' | 'b' | 'c', string | number>;
type JustStrings = OnlyStrings<{ a: string; b: number; c: string }>;
// { a: string; c: string }

Why they combine well: Record builds keyed types; Extract/Exclude filter unions; NonNullable removes absence. Together they cover building, filtering, and cleaning up types. Most real-world type transformations use at least two of them.

Why combining utilities is the norm: Each utility does one thing. Real problems need more. Partial<Record<Status, string>> โ€” a record where entries are optional. Exclude<Status, 'idle'> โ€” a status union without idle. Pick<User, NonNullable<Keys>> โ€” pick from non-null keys. The utilities compose like functions.


A full example

A type-safe status system using all four utilities.

// ============================================
// STATUS DEFINITIONS
// ============================================

type Status = 'idle' | 'loading' | 'ready' | 'error' | 'cancelled';

// Remove one from the union
type ActiveStatus = Exclude<Status, 'idle' | 'cancelled'>;
// 'loading' | 'ready' | 'error'

// Keep only some
type TerminalStatus = Extract<Status, 'ready' | 'error' | 'cancelled'>;
// 'ready' | 'error' | 'cancelled'

// ============================================
// CONFIGURATION RECORDS
// ============================================

interface StatusConfig {
  label: string;
  color: string;
  terminal: boolean;
}

// Full mapping for every status
type StatusConfigMap = Record<Status, StatusConfig>;

const configs: StatusConfigMap = {
  idle: { label: 'Waiting', color: 'gray', terminal: false },
  loading: { label: 'Loading...', color: 'blue', terminal: false },
  ready: { label: 'Ready', color: 'green', terminal: true },
  error: { label: 'Error', color: 'red', terminal: true },
  cancelled: { label: 'Cancelled', color: 'orange', terminal: true }
};

// Partial mapping โ€” some optional
type PartialConfig = Partial<Record<Status, StatusConfig>>;

// Readonly mapping
type FrozenConfig = Readonly<StatusConfigMap>;

// ============================================
// EVENT NAMES
// ============================================

type Events =
  | 'statusChanged'
  | 'statusReset'
  | 'userCreated'
  | 'userDeleted';

type StatusEvents = Extract<Events, `status${string}`>;
// 'statusChanged' | 'statusReset'

type UserEvents = Extract<Events, `user${string}`>;
// 'userCreated' | 'userDeleted'

type NonStatusEvents = Exclude<Events, `status${string}`>;
// 'userCreated' | 'userDeleted'

// ============================================
// HANDLERS
// ============================================

type EventPayloads = {
  statusChanged: { from: Status; to: Status };
  statusReset: {};
  userCreated: { id: number; name: string };
  userDeleted: { id: number };
};

type Handlers = {
  [K in keyof EventPayloads]: (payload: EventPayloads[K]) => void;
};

// ============================================
// NULLABLE VALUES
// ============================================

type MaybeUser = { id: number; name: string } | null | undefined;

function extractName(user: MaybeUser): string {
  // Remove null and undefined
  const u: NonNullable<MaybeUser> = user!;
  return u.name;
}

// ============================================
// USAGE
// ============================================

function transition(from: Status, to: ActiveStatus): string {
  return `${from} โ†’ ${to}`;
}

transition('idle', 'loading');       // โœ…
transition('idle', 'ready');         // โœ…
// transition('idle', 'idle');        // โŒ not ActiveStatus

const handlers: Partial<Handlers> = {
  statusChanged: payload => {
    console.log(`${payload.from} โ†’ ${payload.to}`);
  },
  userCreated: payload => {
    console.log(`created: ${payload.name}`);
  }
};

handlers.statusChanged?.({ from: 'idle', to: 'loading' });
handlers.userCreated?.({ id: 1, name: 'Alice' });

console.log(configs.ready.label);
console.log(extractName({ id: 1, name: 'Bob' }));

What this shows:

  • Exclude removes 'idle' and 'cancelled' from Status
  • Extract keeps only terminal statuses
  • Record<Status, StatusConfig> โ€” exhaustive mapping
  • Partial<Record<...>> โ€” optional mapping
  • Extract<Events, \status${string}`>` โ€” filter by prefix
  • NonNullable<MaybeUser> โ€” remove null and undefined

Each utility does one job. Together they type a real system.

Why this shape: It’s a status machine with typed config, events, and handlers. Adding a status expands the union; every mapping and filter updates automatically. The compiler catches missing entries. That’s the value of these utilities โ€” they derive types from a single source of truth.


Complete Example Session

# ============================================
# PART 1: RECORD
# ============================================

cat > record.ts << 'EOF'
type Status = 'idle' | 'loading' | 'ready' | 'error';

type Messages = Record<Status, string>;

const messages: Messages = {
  idle: 'Waiting',
  loading: 'Loading...',
  ready: 'Ready',
  error: 'Failed'
};

// Missing key fails
// const bad: Messages = { idle: 'x', loading: 'x', ready: 'x' };  // โŒ

// Dynamic keys
type Scores = Record<string, number>;
const scores: Scores = { alice: 10, bob: 20 };
scores.charlie = 30;

console.log(messages.idle, scores.alice);
EOF

npx tsc --noEmit record.ts
# (no errors)

# ============================================
# PART 2: EXCLUDE
# ============================================

cat > exclude.ts << 'EOF'
type Status = 'idle' | 'loading' | 'ready' | 'error';
type ActiveStatus = Exclude<Status, 'idle'>;
// 'loading' | 'ready' | 'error'

const a: ActiveStatus = 'loading';
const b: ActiveStatus = 'ready';
// const c: ActiveStatus = 'idle';  // โŒ

// Remove null from a union
type NotNull = Exclude<string | null, null>;
// string

const d: NotNull = 'hello';

console.log(a, b, d);
EOF

npx tsc --noEmit exclude.ts
# (no errors)

# ============================================
# PART 3: EXTRACT
# ============================================

cat > extract.ts << 'EOF'
type Status = 'idle' | 'loading' | 'ready' | 'error';
type FinalStatus = Extract<Status, 'ready' | 'error'>;
// 'ready' | 'error'

const a: FinalStatus = 'ready';
const b: FinalStatus = 'error';
// const c: FinalStatus = 'idle';  // โŒ

// Extract arrays
type Arr = Extract<string | number[] | boolean | string[], unknown[]>;
// number[] | string[]

const d: Arr = [1, 2];
const e: Arr = ['a', 'b'];

console.log(a, b, d, e);
EOF

npx tsc --noEmit extract.ts
# (no errors)

# ============================================
# PART 4: NONNULLABLE
# ============================================

cat > nonnullable.ts << 'EOF'
type A = NonNullable<string | null>;
// string

type B = NonNullable<string | null | undefined>;
// string

type C = NonNullable<number | undefined>;
// number

const a: A = 'hello';
const b: B = 'world';
const c: C = 42;

console.log(a, b, c);
EOF

npx tsc --noEmit nonnullable.ts
# (no errors)

# ============================================
# PART 5: FILTER BY PATTERN
# ============================================

cat > pattern.ts << 'EOF'
type Events =
  | 'userCreated'
  | 'userDeleted'
  | 'orderPlaced'
  | 'orderShipped';

type UserEvents = Extract<Events, `user${string}`>;
// 'userCreated' | 'userDeleted'

type NonUser = Exclude<Events, `user${string}`>;
// 'orderPlaced' | 'orderShipped'

const a: UserEvents = 'userCreated';
const b: NonUser = 'orderPlaced';

console.log(a, b);
EOF

npx tsc --noEmit pattern.ts
# (no errors)

# ============================================
# PART 6: COMBINING
# ============================================

cat > combined.ts << 'EOF'
type Status = 'idle' | 'loading' | 'ready' | 'error' | 'cancelled';

// Record of all statuses
type StatusConfig = Record<Status, { label: string }>;

// Record of only terminal statuses
type Terminal = Extract<Status, 'ready' | 'error' | 'cancelled'>;
type TerminalConfig = Record<Terminal, { label: string }>;

// Partial record โ€” some optional
type PartialConfig = Partial<Record<Status, { label: string }>>;

const full: StatusConfig = {
  idle: { label: 'Waiting' },
  loading: { label: 'Loading' },
  ready: { label: 'Ready' },
  error: { label: 'Error' },
  cancelled: { label: 'Cancelled' }
};

const terminal: TerminalConfig = {
  ready: { label: 'Ready' },
  error: { label: 'Error' },
  cancelled: { label: 'Cancelled' }
};

const partial: PartialConfig = { idle: { label: 'Waiting' } };

console.log(full.ready, terminal.ready, partial.idle);
EOF

npx tsc --noEmit combined.ts
# (no errors)

# ============================================
# PART 7: NULLABLE PIPELINE
# ============================================

cat > nullable.ts << 'EOF'
interface User {
  id: number;
  name: string;
}

function findUser(id: number): User | null | undefined {
  return id > 0 ? { id, name: 'Alice' } : null;
}

// NonNullable applied to return type
type FoundUser = NonNullable<ReturnType<typeof findUser>>;
// User

function requireUser(id: number): FoundUser {
  const u = findUser(id);
  if (!u) throw new Error('Not found');
  return u;
}

console.log(requireUser(1).name);
EOF

npx tsc --noEmit nullable.ts
# (no errors)

# ============================================
# PART 8: COMPILE AND RUN
# ============================================

npx tsc record.ts exclude.ts extract.ts nonnullable.ts pattern.ts combined.ts nullable.ts
node record.js
# [ Waiting 10 ]

node exclude.js
# [ loading ready hello ]

node extract.js
# [ ready error [ 1, 2 ] [ 'a', 'b' ] ]

node nonnullable.js
# [ hello world 42 ]

node pattern.js
# [ userCreated orderPlaced ]

node combined.js
# [ { label: 'Ready' } { label: 'Ready' } { label: 'Waiting' } ]

node nullable.js
# [ Alice ]

Quick Reference

The Four Utilities

UtilityEffect
Record<K, V>Object with keys K and values V
Exclude<T, U>Remove U from T
Extract<T, U>Keep only U from T
NonNullable<T>Remove null and undefined

Implementations

UtilityDefinition
Record<K, V>{ [P in K]: V }
Exclude<T, U>T extends U ? never : T
Extract<T, U>T extends U ? T : never
NonNullable<T>T extends null | undefined ? never : T

Record Constraint

FormMeaning
Record<K, V>Keys in K
K extends keyof anyK is string | number | symbol
Record<string, V>Any string key
Record<'a' | 'b', V>Only specific keys

Exclude vs Extract

InputExclude<T, U>Extract<T, U>
Matching memberRemovedKept
Non-matchingKeptRemoved
ResultT minus UT โˆฉ U
Reads as“without”“only”

NonNullable Examples

InputOutput
string | nullstring
string | undefinedstring
string | null | undefinedstring
stringstring
nullnever
undefinednever

Distribution

UtilityDistributes
Excludeโœ…
Extractโœ…
NonNullableโœ…
RecordโŒ (mapped, not conditional)

Common Combinations

PatternResult
Partial<Record<K, V>>Optional entries
Readonly<Record<K, V>>Immutable map
Required<Record<K, V>>All required (rarely needed)
Extract<T, \prefix${string}`>`Filter by prefix
Exclude<T, \prefix${string}`>`Remove by prefix
NonNullable<ReturnType<T>>Remove null from function result

Built-in Uses

UtilityUsed by
ExcludeOmit
ExtractUnionToIntersection, Extract filters
NonNullableCommon in generic constraints
RecordCommon in state maps, configs

Error Cases

ErrorCause
Type X not assignable to keyof anyRecord with wrong K
Object literal may only specify known propertiesExtra keys in Record
Property missingRecord requires all keys
Type X not assignableWrong value type

When to Use Each

NeedUtility
Fixed-set mapRecord<K, V>
DictionaryRecord<string, V>
Remove optionExclude<T, U>
Keep only someExtract<T, U>
Remove nullNonNullable<T>
Filter by patternExtract<T, \prefix${string}`>`

NonNullable vs Exclude

AspectNonNullable<T>Exclude<T, null | undefined>
Same resultโœ…โœ…
Intent“not null”“not null and undefined”
ReadabilityClearExplicit

Record with Literal Values

FormMeaning
Record<'a' | 'b', 1>Keys a, b; value 1
Record<'a' | 'b', 'x' | 'y'>Keys a, b; value ‘x’ or ‘y’
Record<string, string>Any key; string value

Distribution Pitfalls

CaseResult
Exclude<never, T>never
Extract<never, T>never
Exclude<any, T>any
Extract<any, T>any
Exclude<unknown, T>unknown

Best Practices

โœ… Do This:

// Use Record for exhaustive maps
type StatusMessages = Record<Status, string>;                       // โœ…

// Use Record<string, V> for dictionaries
type Cache = Record<string, unknown>;                               // โœ…

// Use Exclude to remove options
type ActiveStatus = Exclude<Status, 'idle' | 'cancelled'>;          // โœ…

// Use Extract to keep only some
type FinalStatus = Extract<Status, 'ready' | 'error'>;              // โœ…

// Use NonNullable to remove null
function process(value: string | null): NonNullable<string | null> {
  if (value === null) throw new Error('null');
  return value;
}                                                                    // โœ…

// Combine Extract with template literals for prefix filtering
type UserEvents = Extract<Events, `user${string}`>;                 // โœ…

// Use Partial<Record> for optional maps
type OptionalConfig = Partial<Record<Status, Config>>;              // โœ…

// Use NonNullable<ReturnType<T>> for function results
type User = NonNullable<ReturnType<typeof findUser>>;               // โœ…

// Cast with NonNullable after a check
const value = input as NonNullable<typeof input>;                   // โœ…

โŒ Don’t Do This:

// Don't use Record when keys aren't a union
type Bad = Record<object, string>;  // โŒ object isn't keyof any   // โŒ

// Don't forget Record requires all keys
type Partial = Record<Status, string>;
// const p: Partial = { idle: 'x' };  // โŒ missing keys           // โŒ

// Don't confuse Exclude and Extract
type Wrong = Extract<Status, 'idle'>;  // just 'idle', not removed // โš ๏ธ

// Don't use NonNullable for runtime checks
function f(v: string | null) {
  const s = v as NonNullable<typeof v>;
  // s is still null at runtime if v was null                       // โš ๏ธ
}

// Don't assume NonNullable affects properties
interface U { nick?: string }
type A = NonNullable<U>;  // nick is still optional                // โš ๏ธ

// Don't use Record with non-literal K accidentally
type Bad = Record<string, number>;
// This allows any string key, not just specific ones             // โš ๏ธ

// Don't chain too many utility types
type Complex = NonNullable<Exclude<Extract<...>>>;                  // โš ๏ธ

// Don't expect Extract to remove supertypes
type A = Extract<'a' | 'b', string>;
// 'a' | 'b' โ€” both are strings, neither is removed               // โš ๏ธ

Common Pitfalls

PitfallProblemSolution
Record requires all keysMissing key errorsUse Partial<Record<>>
Confusing Exclude/ExtractWrong direction“without” vs “only”
NonNullable shallowProperties stay optionalApply per-property
Distribution surpriseUnion splitUnderstand it’s per-member
any distributes oddlyUnusual resultAvoid any
never in ExcludeAlways removedExpected
Record with symbol keysRarely what you wantUse literal unions
Over-nesting utilitiesUnreadableExtract named types
Expecting runtime checksTypes onlyValidate at runtime
Record<object, V>InvalidUse string/number/symbol keys

Real-World Examples

1. Record of statuses

type Messages = Record<Status, string>;

2. Dynamic dictionary

type Cache = Record<string, unknown>;

3. Optional record

type PartialMessages = Partial<Record<Status, string>>;

4. Exclude one option

type NoIdle = Exclude<Status, 'idle'>;

5. Exclude multiple

type ActiveOnly = Exclude<Status, 'idle' | 'cancelled'>;

6. Extract only some

type Terminals = Extract<Status, 'ready' | 'error'>;

7. Extract by pattern

type UserEvents = Extract<Events, `user${string}`>;

8. Exclude by pattern

type NonUser = Exclude<Events, `user${string}`>;

9. NonNullable string

type Clean = NonNullable<string | null>;

10. NonNullable return

type User = NonNullable<ReturnType<typeof find>>;

11. Filter union by shape

type ArraysOnly = Extract<Mixed, unknown[]>;

12. Remove a type

type NotBoolean = Exclude<Mixed, boolean>;

13. Record of handlers

type Handlers = Record<EventName, (e: Event) => void>;

14. Readonly record

type FrozenConfig = Readonly<Record<Status, Config>>;

15. NonNullable after check

if (value != null) {
  const v: NonNullable<typeof value> = value;
}

16. Extract numbers from union

type Numbers = Extract<string | number | boolean, number>;

17. Exclude arrays from union

type NotArrays = Exclude<string | number[] | boolean, unknown[]>;

18. Record with literal values

type Directions = Record<'north' | 'south', 0 | 1>;

19. NonNullable of optional property

type Name = NonNullable<User['nickname']>;

20. Chained filter

type UserEvents = Extract<Exclude<Events, `system${string}`>, `user${string}`>;

Visual: Record

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  type Status = 'idle' | 'loading' | 'ready'; โ”‚
โ”‚  type Messages = Record<Status, string>;     โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                  โ”‚
                  โ”‚  for each key
                  โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  {                                           โ”‚
โ”‚    idle: string;                             โ”‚
โ”‚    loading: string;                          โ”‚
โ”‚    ready: string;                            โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ”‚  Every key required                          โ”‚
โ”‚  Every value is string                       โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Exclude

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Exclude<'a' | 'b' | 'c', 'b'>               โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                  โ”‚
                  โ”‚  distribute
                  โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  'a' extends 'b' ? never : 'a'  โ†’ 'a'        โ”‚
โ”‚  'b' extends 'b' ? never : 'b'  โ†’ never      โ”‚
โ”‚  'c' extends 'b' ? never : 'c'  โ†’ 'c'        โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                  โ”‚
                  โ”‚  combine
                  โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  'a' | never | 'c'                           โ”‚
โ”‚  = 'a' | 'c'                                 โ”‚
โ”‚                                              โ”‚
โ”‚  'b' was removed                             โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Extract

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Extract<'a' | 'b' | 'c', 'a' | 'b'>         โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                  โ”‚
                  โ”‚  distribute
                  โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  'a' extends 'a'|'b' ? 'a' : never  โ†’ 'a'    โ”‚
โ”‚  'b' extends 'a'|'b' ? 'b' : never  โ†’ 'b'    โ”‚
โ”‚  'c' extends 'a'|'b' ? 'c' : never  โ†’ never  โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                  โ”‚
                  โ”‚  combine
                  โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  'a' | 'b'                                   โ”‚
โ”‚                                              โ”‚
โ”‚  Only matching members kept                  โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Exclude vs Extract

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  T = 'a' | 'b' | 'c' | 'd'                   โ”‚
โ”‚  U = 'b' | 'd'                               โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Exclude<T, U> = 'a' | 'c'                   โ”‚
โ”‚  โ”€ removed the matching members              โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Extract<T, U> = 'b' | 'd'                   โ”‚
โ”‚  โ”€ kept only the matching members            โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Exclude โˆช Extract = T                       โ”‚
โ”‚  Exclude โˆฉ Extract = never                   โ”‚
โ”‚                                              โ”‚
โ”‚  They're complements                         โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: NonNullable

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  NonNullable<string | null | undefined>      โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                  โ”‚
                  โ”‚  distribute
                  โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  string extends null|undefined? โ†’ string     โ”‚
โ”‚  null extends null|undefined?   โ†’ never      โ”‚
โ”‚  undefined extends null|undefined? โ†’ never   โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                  โ”‚
                  โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  string | never | never                      โ”‚
โ”‚  = string                                    โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Filtering by Pattern

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  type Events =                               โ”‚
โ”‚    | 'userCreated'                           โ”‚
โ”‚    | 'userDeleted'                           โ”‚
โ”‚    | 'orderPlaced'                           โ”‚
โ”‚    | 'orderShipped';                         โ”‚
โ”‚                                              โ”‚
โ”‚  Extract<Events, `user${string}`>            โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                  โ”‚
                  โ”‚  filter
                  โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  'userCreated' matches `user${string}` โœ…    โ”‚
โ”‚  'userDeleted' matches `user${string}` โœ…    โ”‚
โ”‚  'orderPlaced' doesn't โ†’ never               โ”‚
โ”‚  'orderShipped' doesn't โ†’ never              โ”‚
โ”‚                                              โ”‚
โ”‚  Result: 'userCreated' | 'userDeleted'       โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Record with Partial

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  type Status = 'idle' | 'loading' | 'ready'; โ”‚
โ”‚                                              โ”‚
โ”‚  type Full = Record<Status, string>;         โ”‚
โ”‚  type PartialRec = Partial<Record<Status, string>>;โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Full โ€” all keys required                    โ”‚
โ”‚  {                                           โ”‚
โ”‚    idle: string;                             โ”‚
โ”‚    loading: string;                          โ”‚
โ”‚    ready: string;                            โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Partial โ€” any subset allowed                โ”‚
โ”‚  {                                           โ”‚
โ”‚    idle?: string;                            โ”‚
โ”‚    loading?: string;                         โ”‚
โ”‚    ready?: string;                           โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Combining for Filtering

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  type AllEvents =                            โ”‚
โ”‚    | 'userCreated'                           โ”‚
โ”‚    | 'userDeleted'                           โ”‚
โ”‚    | 'orderPlaced'                           โ”‚
โ”‚    | 'orderShipped'                          โ”‚
โ”‚    | 'systemReady';                          โ”‚
โ”‚                                              โ”‚
โ”‚  Extract<AllEvents, `user${string}`>         โ”‚
โ”‚  โ†’ 'userCreated' | 'userDeleted'             โ”‚
โ”‚                                              โ”‚
โ”‚  Exclude<AllEvents, `user${string}`>         โ”‚
โ”‚  โ†’ 'orderPlaced' | 'orderShipped' | 'systemReady'โ”‚
โ”‚                                              โ”‚
โ”‚  Extract<Exclude<AllEvents, `system${string}`>,โ”‚
โ”‚          `order${string}`>                   โ”‚
โ”‚  โ†’ 'orderPlaced' | 'orderShipped'            โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Nullable Pipeline

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  function find(id: number): User | null | undefinedโ”‚
โ”‚                                              โ”‚
โ”‚  ReturnType<typeof find>                     โ”‚
โ”‚  โ†’ User | null | undefined                   โ”‚
โ”‚                                              โ”‚
โ”‚  NonNullable<ReturnType<typeof find>>        โ”‚
โ”‚  โ†’ User                                      โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: When to Use Which

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Building an object with keys from a union?  โ”‚
โ”‚       โ””โ”€โ”€ Record<K, V>                       โ”‚
โ”‚                                              โ”‚
โ”‚  Removing from a union?                      โ”‚
โ”‚       โ””โ”€โ”€ Exclude<T, U>                      โ”‚
โ”‚                                              โ”‚
โ”‚  Keeping only from a union?                  โ”‚
โ”‚       โ””โ”€โ”€ Extract<T, U>                      โ”‚
โ”‚                                              โ”‚
โ”‚  Removing null/undefined?                    โ”‚
โ”‚       โ””โ”€โ”€ NonNullable<T>                     โ”‚
โ”‚                                              โ”‚
โ”‚  Filtering by name pattern?                  โ”‚
โ”‚       โ””โ”€โ”€ Extract<T, `prefix${string}`>      โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Record Internals

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Record<K, V>                                โ”‚
โ”‚  = { [P in K]: V }                           โ”‚
โ”‚                                              โ”‚
โ”‚  K = 'a' | 'b'                               โ”‚
โ”‚  V = number                                  โ”‚
โ”‚                                              โ”‚
โ”‚  Iterate K:                                  โ”‚
โ”‚    P = 'a' โ†’ a: number                       โ”‚
โ”‚    P = 'b' โ†’ b: number                       โ”‚
โ”‚                                              โ”‚
โ”‚  Result:                                     โ”‚
โ”‚  { a: number; b: number }                    โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Decision Flow

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Need a map with known keys?                 โ”‚
โ”‚       โ””โ”€โ”€ Record<K, V>                       โ”‚
โ”‚                                              โ”‚
โ”‚  Need a dictionary?                          โ”‚
โ”‚       โ””โ”€โ”€ Record<string, V>                  โ”‚
โ”‚                                              โ”‚
โ”‚  Removing options from a union?              โ”‚
โ”‚       โ”œโ”€โ”€ Specific values โ”€โ”€โ–บ Exclude        โ”‚
โ”‚       โ””โ”€โ”€ By pattern โ”€โ”€โ–บ Exclude with templateโ”‚
โ”‚                                              โ”‚
โ”‚  Keeping only some options?                  โ”‚
โ”‚       โ”œโ”€โ”€ Specific values โ”€โ”€โ–บ Extract        โ”‚
โ”‚       โ””โ”€โ”€ By pattern โ”€โ”€โ–บ Extract with templateโ”‚
โ”‚                                              โ”‚
โ”‚  Removing null?                              โ”‚
โ”‚       โ””โ”€โ”€ NonNullable<T>                     โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Summary

UtilityEffect
Record<K, V>Object with keys K, values V
Exclude<T, U>Remove U from T
Extract<T, U>Keep only U from T
NonNullable<T>Remove null and undefined

Key takeaways:

  • Record<K, V> builds an object type from a key type and a value type โ€” the map utility
  • Literal union keys make Record exhaustive โ€” every key required
  • Record<string, V> is the dictionary form โ€” any string key
  • Exclude<T, U> removes union members assignable to U โ€” “without”
  • Extract<T, U> keeps only members assignable to U โ€” “only”
  • Exclude and Extract are complements โ€” every member is in exactly one
  • NonNullable<T> removes null and undefined โ€” a specialized Exclude
  • All four are built on mapped types (Record) or conditional types (Exclude, Extract, NonNullable)
  • Distribution makes Exclude and Extract work per union member
  • never disappears from unions โ€” that’s how removal works
  • Combine with template literals to filter unions by name pattern
  • Partial<Record<K, V>> makes entries optional
  • NonNullable<ReturnType<T>> cleans up nullable function results

Remember: These four utilities complete the core vocabulary. Record builds objects from unions. Exclude removes. Extract keeps. NonNullable cleans nulls. They’re used everywhere โ€” state maps, event filters, config tables, nullable cleanups. Each is a small pattern built on conditional or mapped types. Learn them together with the five from the previous chapter and you can express almost any type transformation you’ll encounter.


Stop using slow, ad-bloated tool sites! ๐Ÿคฎ

๐Ÿ”Ž Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
โœ… Finance (Mortgage, Interest, Inflation)
โœ… Tech (Base64, JSON, Dev Suite, IP)
โœ… Health (BMI, BMR, TDEE)
โœ… Productivity (Timer, Workspace, QR)

โšก๏ธ Fast & Private
๐Ÿ”’ No data leaves your device
๐Ÿ’Ž 100% Free

๐Ÿ”— Use it now: https://tools.kandz.me
๐Ÿ”– Bookmark itโ€”youโ€™ll need it later!