In TypeScript, this code fails to compile — why, and what are the ways to fix it? ```ts type Shape = { kind: 'circle'; r: number } | { kind: 'square'; size: number }; const c = { kind: 'circle', r: 1 }; const s: Shape = c; ```
answer
- look at the inferred type, not the value
- const freezes the binding, not the field
- the property could still be reassigned
- three fixes: annotate, as const, satisfies
- string is not assignable to "circle"
basics
~10 sInference widens the object's kind property to string, and string is not assignable to the literal type 'circle'. Fix it by annotating the variable as Shape, adding as const, or using satisfies Shape.
solid answer
~50 sThe object literal is not contextually typed by anything, so TypeScript infers a *mutable* property and widens the literal `'circle'` to `string`. The resulting type is `{ kind: string; r: number }`, and on the assignment the compiler reports that `string` is not assignable to `"circle"`. Note the widening is about the property, not the `const`: `const tag = 'circle'` keeps the literal type because the binding itself cannot change, but `c.kind = 'square'` is legal, so the property must be wide. Three fixes: annotate the variable, `const c: Shape = { ... }`, which contextually types the literal; add `as const` to the object or `'circle' as const` to the value, freezing the property to its literal type; or write `{ ... } satisfies Shape`, which checks against `Shape` while keeping the narrow inferred type. The same trap hits helper functions that build union members and have no declared return type.
code
typescript · 15 linestype Shape = { kind: 'circle'; r: number } | { kind: 'square'; size: number };
// widened: { kind: string; r: number } -> not assignable
const wide = { kind: 'circle', r: 1 };
// fix 1: contextual type from the annotation
const a: Shape = { kind: 'circle', r: 1 };
// fix 2: const assertion pins every literal
const b = { kind: 'circle', r: 1 } as const;
const b2: Shape = b;
// fix 3: checked, but keeps the narrow inferred type
const c = { kind: 'circle', r: 1 } satisfies Shape;
console.log(c.r.toFixed(2), a.kind, b2.kind, typeof wide.kind);go deeper
Recognise that the error is about the kind property, not the shape of the object, and know the quickest fix: annotate the variable with the union type so the literal is checked in place.
Explain literal widening: an unannotated object literal has mutable properties, so 'circle' becomes string. Contrast it with const tag = 'circle', and show all three fixes with what each does to the variable's own type.
Spot the pattern where it actually bites — factory functions and helpers with inferred return types — and argue for annotating the return type so the error lands at the definition rather than at every call site.
Set the house rule: union members get pinned at construction, by declared return type or satisfies, so a widened tag can never travel through the codebase unnoticed and quietly disable narrowing downstream.
## What the compiler actually infers When TypeScript infers the type of an object literal with no *contextual type* — no annotation on the variable, no parameter type, no declared return type to guide it — it applies **literal widening** to mutable properties. The literal `'circle'` has the type `"circle"` at the expression level, but because `c.kind` is a mutable property that could later be assigned any string, the inferred property type is widened to `string`: ```ts const c = { kind: 'circle', r: 1 }; // ^? { kind: string; r: number } c.kind = 'square'; // legal — which is exactly why kind is `string` ``` Assigning that to `Shape` then fails: the union requires `kind` to be `"circle"` or `"square"`, and `string` is assignable to neither. The error points at the property: `string` is not assignable to type `"circle"`. ## Why `const` does not save you This is the part candidates get wrong. `const` on a *primitive* binding does prevent widening, because the binding can never be reassigned: ```ts const tag = 'circle'; // type "circle" let tag2 = 'circle'; // type string ``` But `const` on an object only freezes the *binding*, not the properties. The object's fields remain mutable, so their inferred types widen. `const` and `readonly` are different guarantees, and only the second one is about the property. ## Fix 1 — annotate the variable ```ts const c: Shape = { kind: 'circle', r: 1 }; ``` The annotation gives the literal a contextual type. Contextual typing suppresses widening — the compiler checks `'circle'` against the expected literal instead of inferring from scratch — and it also turns on excess-property checking, so a typo in a field name is caught at the literal. The downside is that `c` is now typed as the whole union: `c.r` is an error until you narrow, because the declared type is `Shape`, not the circle member. ## Fix 2 — `as const` ```ts const c = { kind: 'circle', r: 1 } as const; // ^? { readonly kind: "circle"; readonly r: 1 } const s: Shape = c; // fine ``` A const assertion tells the compiler to infer the narrowest type: every literal keeps its literal type and every property becomes `readonly`. The `readonly` modifiers do not block the assignment — property readonly-ness is not part of assignability here — but they do make `c` genuinely immutable in your own code, which is usually what you wanted. If you only need the tag pinned, `{ kind: 'circle' as const, r: 1 }` widens `r` but keeps `kind` narrow. ## Fix 3 — `satisfies` ```ts const c = { kind: 'circle', r: 1 } satisfies Shape; // ^? { kind: "circle"; r: number } c.r.toFixed(2); // allowed — c is still the circle member ``` `satisfies` checks the expression against `Shape` — you get the same error as an annotation if the shape is wrong or the tag is misspelled — but the variable keeps its *inferred*, narrower type rather than being widened to the union. That is the best of both when you want the check and still want to reach into the specific member afterwards. Because it supplies a contextual type, the tag does not widen. ## The same trap, in a helper The most common production form is not a variable at all — it is a factory whose return type is inferred: ```ts function circle(r: number) { return { kind: 'circle', r }; // inferred { kind: string; r: number } } const s: Shape = circle(1); // error ``` Declare the return type (`function circle(r: number): Shape`), or return `{ ... } as const`, or add `satisfies Shape` to the returned literal. Annotating the return type is usually best: it documents the contract and it makes the error appear inside the factory, where the bug is, instead of at every call site. ## Why this is worth knowing A widened tag is a *silent* failure of intent everywhere except the one assignment that catches it. If the value never meets an annotated `Shape`, nothing complains — you just have an object whose `kind` is `string`, and any later `switch` on it narrows nothing. Interviewers use this snippet because it separates candidates who have memorised the syntax of discriminated unions from those who understand that the discriminant only works when the *inferred* type kept the literal. A rule of thumb: whenever a union member is constructed somewhere other than directly at an annotated position, pin the tag — with an annotation, `as const`, or `satisfies`.
- Why does `const tag = 'circle'` keep the literal type while `const o = { kind: 'circle' }` does not?Widening tracks what can change. A `const` primitive binding can never be reassigned, so the compiler safely keeps the narrow type `"circle"`. An object's properties stay mutable under `const` — `o.kind = 'square'` is legal — so inferring `"circle"` for the property would be unsound and it widens to `string`. `as const` or a `readonly` property is what pins it.
- When would you reach for `satisfies Shape` instead of annotating the variable `: Shape`?When you want the check but not the widening. `const c: Shape = ...` types `c` as the whole union, so `c.r` is inaccessible until you narrow. `satisfies Shape` validates the literal against the union — same errors for a bad shape or a misspelled tag — while `c` keeps its inferred type `{ kind: "circle"; r: number }`, so you can use the member's fields directly.
- Does `as const` on a member break assigning it into a mutable-typed union?No. `as const` makes every property `readonly`, but property readonly-ness is not considered when checking object assignability, so a `{ readonly kind: "circle"; readonly r: 1 }` value is assignable to `{ kind: 'circle'; r: number }`. Where it does bite is arrays and tuples: a `readonly` array is not assignable to a mutable array type.
saying these in an interview costs you the question
- Says const already makes the property a literal type
- Reaches for `as Shape`, an unchecked assertion, as the fix
- Claims the union declaration itself is wrong
- Thinks `as const` fails because of readonly properties
- Blames strictNullChecks or another compiler flag