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
Recordis a mapped type: It has the shape{ [P in K]: V }โ iterateK, 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:
Excluderemoves what matches;Extractkeeps what matches. They’re complements, but naming matters. When the set you’re filtering by is small,Extractreads better. When the exception set is small,Excludedoes. 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:
Excluderemoves'idle'and'cancelled'fromStatusExtractkeeps only terminal statusesRecord<Status, StatusConfig>โ exhaustive mappingPartial<Record<...>>โ optional mappingExtract<Events, \status${string}`>` โ filter by prefixNonNullable<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
| Utility | Effect |
|---|---|
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
| Utility | Definition |
|---|---|
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
| Form | Meaning |
|---|---|
Record<K, V> | Keys in K |
K extends keyof any | K is string | number | symbol |
Record<string, V> | Any string key |
Record<'a' | 'b', V> | Only specific keys |
Exclude vs Extract
| Input | Exclude<T, U> | Extract<T, U> |
|---|---|---|
| Matching member | Removed | Kept |
| Non-matching | Kept | Removed |
| Result | T minus U | T โฉ U |
| Reads as | “without” | “only” |
NonNullable Examples
| Input | Output |
|---|---|
string | null | string |
string | undefined | string |
string | null | undefined | string |
string | string |
null | never |
undefined | never |
Distribution
| Utility | Distributes |
|---|---|
Exclude | โ |
Extract | โ |
NonNullable | โ |
Record | โ (mapped, not conditional) |
Common Combinations
| Pattern | Result |
|---|---|
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
| Utility | Used by |
|---|---|
Exclude | Omit |
Extract | UnionToIntersection, Extract filters |
NonNullable | Common in generic constraints |
Record | Common in state maps, configs |
Error Cases
| Error | Cause |
|---|---|
Type X not assignable to keyof any | Record with wrong K |
Object literal may only specify known properties | Extra keys in Record |
Property missing | Record requires all keys |
Type X not assignable | Wrong value type |
When to Use Each
| Need | Utility |
|---|---|
| Fixed-set map | Record<K, V> |
| Dictionary | Record<string, V> |
| Remove option | Exclude<T, U> |
| Keep only some | Extract<T, U> |
| Remove null | NonNullable<T> |
| Filter by pattern | Extract<T, \prefix${string}`>` |
NonNullable vs Exclude
| Aspect | NonNullable<T> | Exclude<T, null | undefined> |
|---|---|---|
| Same result | โ | โ |
| Intent | “not null” | “not null and undefined” |
| Readability | Clear | Explicit |
Record with Literal Values
| Form | Meaning |
|---|---|
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
| Case | Result |
|---|---|
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
| Pitfall | Problem | Solution |
|---|---|---|
| Record requires all keys | Missing key errors | Use Partial<Record<>> |
| Confusing Exclude/Extract | Wrong direction | “without” vs “only” |
| NonNullable shallow | Properties stay optional | Apply per-property |
| Distribution surprise | Union split | Understand it’s per-member |
any distributes oddly | Unusual result | Avoid any |
never in Exclude | Always removed | Expected |
| Record with symbol keys | Rarely what you want | Use literal unions |
| Over-nesting utilities | Unreadable | Extract named types |
| Expecting runtime checks | Types only | Validate at runtime |
| Record<object, V> | Invalid | Use 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
| Utility | Effect |
|---|---|
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 keyExclude<T, U>removes union members assignable toUโ “without”Extract<T, U>keeps only members assignable toUโ “only”ExcludeandExtractare complements โ every member is in exactly oneNonNullable<T>removesnullandundefinedโ a specializedExclude- All four are built on mapped types (
Record) or conditional types (Exclude,Extract,NonNullable) - Distribution makes
ExcludeandExtractwork per union member neverdisappears from unions โ that’s how removal works- Combine with template literals to filter unions by name pattern
Partial<Record<K, V>>makes entries optionalNonNullable<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!