In TypeScript 5.x, why does the type `'red' | 'blue' | string` behave exactly like `string`, and what is the `(string & {})` idiom that library authors write to avoid it?
answer
- a union drops redundant members
- the supertype swallows the literals
- suggestions disappear, acceptance does not
- not identical to string, just interchangeable
- ergonomics, not safety
basics
~20 sA union absorbs members that are subtypes of another member, and every string literal type is a subtype of string, so the literals are reduced away and only string remains. Writing (string & {}) instead of string blocks that reduction and keeps editor suggestions.
solid answer
~50 sWhen TypeScript normalises a union it drops any member that is a subtype of another member. String literal types are subtypes of `string`, so `'red' | 'blue' | string` reduces to plain `string`: the annotation still accepts every string, but the literals are gone, and with them the editor's autocomplete suggestions — which was usually the only reason someone wrote them. The workaround, common in UI libraries in the TypeScript 5.x line, is `'red' | 'blue' | (string & {})`. The intersection is still assignable to and from `string`, so any string is accepted, but it is not *identical* to `string`, so the reduction step leaves the literal members in place and the editor keeps suggesting them. It is an ergonomics trick, not a safety feature: nothing is rejected that plain `string` would have accepted, and hover text and error messages get uglier.
code
typescript · 9 linestype Collapsed = 'red' | 'blue' | string; // reduces to: string
type Suggested = 'red' | 'blue' | (string & {}); // literals survive
declare function paint(color: Suggested): void;
paint('red'); // editor suggests this
paint('rebeccapurple'); // still accepted — no safety added
const anything: Collapsed = 'whatever';go deeper
Know that adding string to a union of string literals makes the annotation equivalent to string, so the listed values stop constraining anything.
Explain the mechanism: union normalisation discards members that are subtypes of another member, and every string literal is a subtype of string. Say what is lost — the editor suggestions.
Produce the (string & {}) idiom, explain why the intersection escapes reduction, and be explicit that it buys autocomplete rather than safety while costing readable diagnostics.
Own the API-shape decision: closed union, separate parameters for the open case, or the suggestion idiom — and set where each is acceptable in a shared package given the diagnostics cost it imposes on every consumer.
## The reduction rule TypeScript normalises union types when it constructs them, and part of that normalisation is discarding redundant members: if member A is a subtype of member B, A adds nothing to the set B already describes, so A is removed. ```ts type Collapsed = 'red' | 'blue' | string; // displays and behaves as: string ``` Every string literal type is a subtype of `string`, so `'red'` and `'blue'` are absorbed. The set of values genuinely is the same — the union was always "any string" once `string` was in it — and the compiler is not being clever or wrong. It is telling you the truth: your annotation says nothing more than `string`. The same reduction is why `type T = 200 | number` is just `number`, and why `boolean | true` is just `boolean`. ## Why anyone writes that union anyway The motivation is nearly always **autocomplete on an open-ended value**. A UI library has a `color` prop with a handful of theme names but must also accept arbitrary CSS colours; an HTTP wrapper has well-known header names but must accept custom ones. The author wants the editor to suggest the known values while still accepting anything, so they write `'red' | 'blue' | string` — and get plain `string`, with no suggestions at all. The literal members were the entire point, and the reduction ate them. ## The idiom ```ts type Suggested = 'red' | 'blue' | (string & {}); declare function paint(color: Suggested): void; paint('red'); // suggested by the editor paint('rebeccapurple'); // still accepted ``` `string & {}` is an intersection type. In assignability terms it is interchangeable with `string` — every string satisfies it and it satisfies `string` — but it is not the *same type object* as `string`, so the subtype-reduction step does not recognise the literal members as redundant against it. They survive into the union, and the editor lists them while the parameter still admits any string. The pattern is often factored into a helper, conventionally named something like `LiteralUnion`, that takes the literal union and the base primitive as type parameters. Variants such as `string & Record<never, never>` exist and rely on the same mechanism. ## What it does and does not buy you It is a pure **ergonomics** trick. Concretely: - It **does not** add type safety. `paint('rebeccapurpel')` — a typo — compiles fine, exactly as it would with plain `string`. If you want typos caught, you want a closed literal union with no `string` member at all, and callers who genuinely need arbitrary values should go through a different parameter or an explicit escape. - It **degrades diagnostics**. Hover text and error messages show `'red' | 'blue' | (string & {})`, which is noise for every consumer of the API, and mismatches involving it read worse than a plain `string` mismatch. - It **depends on compiler normalisation internals**. This behaviour holds in the TypeScript 5.x line and has for several major versions, but it is not a specified language feature — it works because of *how* reduction compares members, not because anyone designed an opt-out. ## The design question behind it Before reaching for the idiom, ask whether the type should be open at all. Three honest options: 1. **Closed union** — `'red' | 'blue'`. Maximum safety, real errors on typos, and callers with an unusual value are blocked. Right for an internal API where the set really is finite. 2. **Two parameters or a wrapper** — a `theme` prop taking the closed union plus a separate `customColor` prop taking `string`. More verbose, but every value's intent is explicit and both halves are properly typed. 3. **The `(string & {})` union** — right when the value genuinely is open-ended (CSS colours, header names, locale tags) and the literals are *hints*, not constraints. Be honest in the docs that they are hints. A senior answer names the mechanism, produces the idiom, and then says which of the three the situation calls for rather than treating the trick as the default. ## Runtime, as always: nothing Both forms erase completely. `paint` receives an ordinary string, and a value that arrived from a config file or an API response was never checked against anything. If invalid colours must be rejected at runtime, that is a validation step you write, not something the union provides.
- Does the `(string & {})` form reject any value that plain `string` would accept?No — it accepts exactly the same set of values, so a typo like `'rebeccapurpel'` still compiles. The only change is that the literal members survive union reduction and the editor keeps suggesting them. Treat it as autocomplete tooling, and if you actually need invalid values rejected, use a closed union with no open member.
- What is the cost of shipping this idiom in a public API's type surface?Diagnostics. Every hover and error message on that parameter shows `(string & {})`, which consumers must learn to read past, and the type is harder to compose with. It also depends on compiler normalisation internals rather than a specified feature, so it is a pattern to use deliberately in a few high-traffic props, not everywhere.
- Does the same reduction happen with numeric literals?Yes — `200 | 404 | number` reduces to `number` for exactly the same reason, since numeric literal types are subtypes of `number`. The analogous workaround is `number & {}`. The rule is general: any union member that is a subtype of another member is discarded during normalisation.
saying these in an interview costs you the question
- Thinks the literals still restrict values in 'red' | 'blue' | string
- Calls the reduction a compiler bug rather than subtype normalisation
- Believes (string & {}) rejects strings outside the listed literals
- Expects the union to be validated against incoming data at runtime
- Reaches for the idiom by default instead of a closed union