The Set Operators: Record, Exclude, Extract

Advanced · 20 min read · ▶ live playground · ✦ checkpoint

Set utilities work on key sets and value unions. Record<K, V> builds a dictionary from a key union, Exclude<T, U> removes union members, Extract<T, U> keeps matching union members, and NonNullable<T> removes null and undefined.

Last lesson reshaped object properties with Partial, Pick, and Omit. This one is about a different move: describe the allowed set of keys or values, then let TypeScript check every use against that set.

Record For Known Keys

Record<K, V> means "for every key in K, there is a property with value type V." When K is a literal union, Record becomes an exhaustive checklist.

typescript — playgroundtype-checked
⌘/Ctrl + Enter to run

This is more precise than a plain object type with optional properties. reader, author, and owner are not suggestions; they are the full key set. Add a new role to the Role union and this map becomes one of the first places the checker tells you to update.

That distinction is the reason Record is so useful in permission systems. Use a closed key union when the product has a known list of roles, statuses, routes, or plans. Use a broad string dictionary when keys are genuinely open-ended.

Broad Records And Index Signatures

Record<string, T> means "a string-keyed dictionary whose values are T." The older index-signature spelling says the same broad thing:

type FeatureFlagsRecord = Record<string, boolean>;
 
type FeatureFlagsIndex = {
  [flagName: string]: boolean;
};

Both are structural object types. Pick the spelling that communicates best in context. Record<string, boolean> reads nicely when you are already using utility types; an index signature is clearer when you need to name the key inside the type body.

typescript — playgroundtype-checked
⌘/Ctrl + Enter to run

Notice what broad dictionaries do and do not promise. They do promise that provided values are booleans. They do not promise that "unknownFlag" exists at runtime. For truly open dictionaries, code still needs a runtime missing-key decision, like the "off" fallback above.

You will also see Record<string, unknown> in guards for unknown objects. Read it as "a string-keyed bag of values we have not narrowed yet." Record gives the checker a dictionary view after your runtime check; it is not runtime validation by itself.

Exclude And Extract

Exclude<T, U> removes union members from T that fit U. Extract<T, U> keeps only union members from T that fit U.

Section 10 showed the conditional-type machinery that makes this possible. In daily code, reach for the built-ins and name the business set you mean.

typescript — playgroundtype-checked
⌘/Ctrl + Enter to run

This is set logic in the literal sense. Start with all permissions. Extract the high-risk subset. Exclude that subset from the original union to get the everyday subset. If someone adds "workspace:archive" to Permission, the named subsets make the review obvious: should it be high-risk or everyday?

The same move works on discriminated unions too. Extract<Event, { kind: "payment" }> keeps the payment event member. Exclude<Event, { kind: "debug" }> removes debug events from a public stream. You are not changing runtime values; you are naming slices of a union so APIs can accept only the slice they mean.

NonNullable

NonNullable<T> is the cleanup utility for a common boundary inside your own program: a value may be missing before a check, then must be present after the check.

It removes only null and undefined. It does not remove empty strings, 0, false, or any other value.

typescript — playgroundtype-checked
⌘/Ctrl + Enter to run

This is a small utility, but it keeps signatures honest. MaybeSelectedRole is the state before a selection exists. SelectedRole is the state after code has handled the missing case. The two names prevent a checked value from being confused with an unchecked one.

Building Permission Systems

A practical permission model usually combines all four tools:

  • A closed role union: "reader" | "author" | "owner".
  • A permission union: "post:read" | "post:write" | ....
  • Record<Role, Permission[]> for exhaustive role coverage.
  • Extract and Exclude for named permission subsets.
  • NonNullable when UI or request state starts as maybe-selected.

The important habit is to name the set once, then derive the related sets from it. Do not hand-copy "billing:update" into five unrelated aliases. Put it in Permission, then use Extract and Exclude to show how each API slices that source union.

These utilities operate on union sets and dictionary keys. Next lesson changes target again: ReturnType, Parameters, and Awaited inspect function signatures and promises.

Checkpoint

Answer all three to mark this lesson complete

+50 XP on completion