In TypeScript, a helper `type Stripped<T> = Omit<T, 'id'>` applied to a discriminated union collapses it into a single object type and loses the members' own properties. Why does that happen, and how do you keep the union?
answer
- keyof a union keeps only shared keys
- no naked T means no distribution
- one line of conditional restores it
- Partial behaves differently from Omit
- name the distributing variant explicitly
basics
~20 sOmit is not distributive: T is used inside keyof and Pick, never as a conditional's naked checked type, and keyof of a union yields only the shared keys. Wrap it yourself — T extends unknown ? Omit<T, K> : never — to apply it per member.
solid answer
~50 s`Omit` is not distributive. It is defined as `Pick<T, Exclude<keyof T, K>>`, so `T` appears inside `keyof` and `Pick`, never as the naked checked type of a conditional. `keyof` over a union gives only the keys common to *every* member, so a union of a circle and a square shape collapses to just the shared `kind` and `id` keys, and after removing `id` you are left with one flat object that has lost `radius`, `side`, and the ability to narrow. The fix is to distribute explicitly with your own conditional: `type DistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never`. The naked `T` makes the compiler run `Omit` once per member and union the results, preserving the discriminated union. `Partial` and `Readonly` need no such wrapper, because homomorphic mapped types already distribute over union members.
code
typescript · 12 linestype Circle = { kind: 'circle'; id: string; radius: number };
type Square = { kind: 'square'; id: string; side: number };
type Shape = Circle | Square;
type Collapsed = Omit<Shape, 'id'>; // { kind: 'circle' | 'square' }
type DistributiveOmit<T, K extends PropertyKey> =
T extends unknown ? Omit<T, K> : never;
type Preserved = DistributiveOmit<Shape, 'id'>;
const s: Preserved = { kind: 'square', side: 2 };go deeper
Know that removing a key from a union of object types can quietly change it into one flat type, and check the hover result rather than assuming the union survived.
Be ready to explain both ingredients — keyof over a union yields only shared keys, and Omit has no naked conditional — and to write the T extends unknown ? Omit<T, K> : never wrapper.
Expect to trace lost narrowing back to a shared helper that collapsed a union, and to decide per helper whether per-member transformation or a common-keys view is the honest answer.
Own the naming and review convention: distributing and collapsing helpers must be distinguishable at the call site, since a collapse in a shared types package removes exhaustiveness checking across every consumer with no error at the source.
## The symptom A discriminated union goes into a helper and something structurally different comes out: ```ts type Circle = { kind: 'circle'; id: string; radius: number }; type Square = { kind: 'square'; id: string; side: number }; type Shape = Circle | Square; type Stripped = Omit<Shape, 'id'>; // => { kind: 'circle' | 'square' } ``` `radius` and `side` are gone entirely, and what is left is a single object type rather than two alternatives, so `switch (s.kind)` no longer narrows to anything useful downstream. ## Why: two rules combining **Rule one — `keyof` over a union is the intersection of keys.** That is sound rather than arbitrary: a value of type `Circle | Square` is guaranteed to have only the properties every member has, so the only keys you may safely read are the shared ones. `keyof Shape` is `'kind' | 'id'`, not the four distinct keys. **Rule two — `Omit` never puts `T` in a distributive position.** Its declaration is effectively: ```ts type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>; type Pick<T, K extends keyof T> = { [P in K]: T[P] }; ``` Distribution only happens when a **naked type parameter** sits in the checked position of a conditional type and is instantiated with a union. Here `T` is fed to `keyof` and indexed inside a mapped type; there is no conditional over a bare `T` anywhere, so the union is processed as one type. Put together: `keyof Shape` is `'kind' | 'id'`, `Exclude<'kind' | 'id', 'id'>` is `'kind'`, and `Pick<Shape, 'kind'>` builds one object `{ kind: 'circle' | 'square' }`. Every step is correct in isolation; the surprise is that the union never survived to step three. ## The fix: distribute on purpose Add the conditional that `Omit` lacks: ```ts type DistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never; type Better = DistributiveOmit<Shape, 'id'>; // { kind: 'circle'; radius: number } | { kind: 'square'; side: number } ``` The condition `T extends unknown` is trivially true — its only job is to place `T` in a naked checked position so the compiler substitutes each member, runs `Omit` on that member alone, and unions the two results. `T extends any` is used interchangeably. `PropertyKey` is the built-in alias for `string | number | symbol`, which is the right constraint here because the keys being removed may not exist on every member. The same one-line wrapper generalises: any helper that must be applied per member gets `T extends unknown ? Helper<T> : never` around it. ## Which built-ins already distribute This is the part worth knowing precisely, because the behaviour is not uniform: - `Partial<T>`, `Required<T>` and `Readonly<T>` are *homomorphic* mapped types — written as `{ [P in keyof T]: ... }`, mapping over `keyof T` directly. Homomorphic mapped types distribute over union members automatically, so `Partial<Circle | Square>` is `Partial<Circle> | Partial<Square>` and the union survives. - `Pick<T, K>` and `Omit<T, K>` map over an explicit key set rather than `keyof T`, so they are not homomorphic in that sense and collapse a union into a single object. - `Exclude<T, U>`, `Extract<T, U>` and `NonNullable<T>` are built on naked conditionals and are inherently distributive — indeed they are only meaningful because of it. A candidate who says "utility types distribute" or "utility types don't distribute" is wrong either way; the answer depends on how each one is written. ## Judgment: when you actually want each Non-distributive is sometimes exactly right. If the point of a helper is "what can I safely read off a value of this type", collapsing to the common keys is the truthful answer, and distributing would invent a union of shapes the value might not have. If the point is "transform each variant of my model", you want distribution. The practical rule: transforms over a **discriminated union** should distribute, because the whole value of the discriminated union — narrowing by the discriminant, exhaustive handling of variants — depends on it staying a union. Collapsing it once, early, in a shared helper, silently removes narrowing everywhere downstream, and nothing errors at the helper's definition. Name the distributing variant explicitly (`DistributiveOmit`) rather than shadowing `Omit`, so a reader knows which behaviour they are getting without checking the definition. ## Erasure All of this is compile-time only. `DistributiveOmit` emits nothing; there is no per-member loop at run time and no cost beyond compiler work. What changes is only the type the checker reports — and therefore whether a `switch` on the discriminant still narrows.
- Does `Partial` need the same wrapper on a union?No. `Partial` is a homomorphic mapped type — `{ [P in keyof T]?: T[P] }` — and homomorphic mapped types distribute over union members on their own, so `Partial<Circle | Square>` stays `Partial<Circle> | Partial<Square>`. `Readonly` and `Required` behave the same way. `Pick` and `Omit` map over an explicit key set instead, which is why only they need help.
- Why write `T extends unknown` rather than a meaningful condition?Because the conditional exists only to place `T` in a naked checked position and trigger distribution — the condition itself must always be true so no member is dropped. `T extends unknown` and `T extends any` both do that; `unknown` reads more honestly as "no constraint intended". The false branch is `never`, which is unreachable and vanishes from the union anyway.
- Is collapsing a union ever the behaviour you want?Yes. If a helper answers "what may I safely read from a value of this type", the intersection of keys is the correct, sound answer, and distributing would suggest properties a given value may not have. Distribution is for transforming each variant of a model; collapse is for describing what all variants have in common.
saying these in an interview costs you the question
- Assumes every built-in utility type distributes over unions
- Blames Omit for a bug instead of keyof over a union
- Applies Omit twice hoping the union returns
- Thinks the discriminant property alone preserves narrowing
- Says distribution costs a runtime loop over members