skip to content

Given `interface Point { x: number; y: number }` in TypeScript, why does `const p: Point = { x: 1, y: 2, z: 3 }` fail with "Object literal may only specify known properties", while `const tmp = { x: 1, y: 2, z: 3 }; const p: Point = tmp;` compiles cleanly?

level: middleimportance: must knowfreq 74%

answer

  1. two rules, not one
  2. the literal, not the object
  3. widening throws something away
  4. assertion silences, does not fix
  5. typo guard at the birthplace

basics

~20 s

TypeScript applies an extra excess-property check to fresh object literals assigned straight to a typed target. Storing the literal in a variable first widens it and drops that freshness, leaving only ordinary structural assignability, which happily permits extra properties.

solid answer

~50 s

Structurally, `{ x: 1, y: 2, z: 3 }` is assignable to `Point` — it has everything `Point` requires, and extra members are normally irrelevant. On top of that rule the compiler adds a second, deliberately stricter one: an object literal written directly at the assignment site has a *fresh* type, and while a type is fresh the compiler rejects any property the target does not declare. That is a typo guard — if you write `z` where the target has no `z`, nothing can ever read it through that type, so it is almost certainly a mistake. Freshness is a one-shot property of the literal expression: assign it to a variable with no annotation and the type widens, freshness is discarded, and the later assignment sees only the permissive structural rule. The same freshness check fires on call arguments, `return` expressions and nested literals.

code

typescript · 9 lines
typescript
interface Point { x: number; y: number }

// @ts-expect-error excess property check on a fresh object literal
const direct: Point = { x: 1, y: 2, z: 3 };

const tmp = { x: 1, y: 2, z: 3 };
const viaVariable: Point = tmp; // no error: tmp's type is not fresh

console.log(direct, viaVariable);

go deeper

for a junior

Recognise the message "Object literal may only specify known properties" and know the first thing to check is a misspelled key. Say plainly that the fix is to correct the name or to declare the property on the type.

for a middle

Explain the mechanics: object literals get a fresh type, freshness triggers an excess-property check on top of ordinary structural assignability, and widening into an unannotated variable or applying an assertion discards it. Name the positions where it fires.

for a senior

Show the judgment about which escape you allow in a codebase. Be able to argue why silencing with as trades a real typo guard for a compiling build, and when widening the declared type is the correct answer instead.

for a principal

Own the policy angle: this check is a deliberately unsound convenience, so decide where your codebase relies on it versus where boundary data must be validated at run time, and how lint rules or review standards keep assertions from eroding it.

## The base rule: structural assignability TypeScript's assignability is structural. A value is assignable to a target type when it has at least the members the target requires, with compatible types. Extra members are simply not the target type's business — that is what makes duck typing work, and it is why a `Point`-typed parameter cheerfully accepts an object that also carries `z`, `label` and a `toString` override. If that were the whole story, `const p: Point = { x: 1, y: 2, z: 3 }` would compile. It does not, and the reason is a second rule layered on top. ## Freshness and the excess property check When you write an object literal expression, the compiler gives its inferred type a flag usually called *freshness*. While a type is fresh, assigning it to a target performs an additional check beyond assignability: every property in the literal must exist in the target type. A property that does not exist there produces error 2353, `Object literal may only specify known properties, and 'z' does not exist in type 'Point'`. When the offending name is close to a real one the compiler will often add a "did you mean" suggestion, which is the whole point of the feature — catching `colour` when the type declares `color`. The motivation is reachability. If the literal is consumed only through the target type, no code can ever legally read the extra property, so writing it is either a typo or a misunderstanding of the contract. The compiler would rather ask than silently drop it on the floor. ```ts interface Point { x: number; y: number } const a: Point = { x: 1, y: 2, z: 3 }; // error 2353 function move(p: Point) {} move({ x: 1, y: 2, z: 3 }); // error 2353 — argument literals too function origin(): Point { return { x: 0, y: 0, z: 0 }; // error 2353 — return position too } ``` The check applies wherever a literal meets a contextual type: annotated variable declarations, call arguments, `return` statements against a declared return type, array literal elements against an annotated array type, and object literals nested inside another fresh literal. ## How freshness is lost Freshness belongs to the literal *expression*, not to the object. Two things discard it: 1. **Widening on assignment to an unannotated variable.** `const tmp = { x: 1, y: 2, z: 3 }` infers `{ x: number; y: number; z: number }` — a perfectly ordinary, no-longer-fresh object type. The subsequent `const p: Point = tmp` is judged by structural assignability alone, and passes. 2. **A type assertion.** `{ x: 1, y: 2, z: 3 } as Point` also removes freshness, which is why `as` "fixes" the error — by disabling the check rather than by resolving anything. ```ts const tmp = { x: 1, y: 2, z: 3 }; const p: Point = tmp; // fine — tmp's type is not fresh ``` This is a real, intentional hole in the check, and interviewers ask about it precisely because a candidate who only memorised "TypeScript rejects extra properties" cannot explain it. The design accepts the hole: once the value lives in a variable, other code may genuinely be using `z` through the wider type, so complaining would be wrong. ## What the check is not - **It is not runtime behaviour.** Types are erased; the emitted JavaScript still contains `z`, and nothing checks anything at run time. - **It is not stripping.** TypeScript never deletes a property to make an assignment fit. - **It is not a soundness guarantee.** As shown, an intermediate variable smuggles the property past it, and so does `as`. ## Making the extra property legal on purpose When the extra key is genuinely wanted, the honest fixes change the *type*, not the expression: declare `z` on `Point` (or on a wider type used at that site), or give the target an index signature so unknown keys are part of the contract. Reaching for `as` to silence the message keeps the code compiling while throwing away the very protection you were being offered — and it will silence the next typo too. ## How to say it in an interview Name the two layers explicitly: permissive structural assignability underneath, an excess-property check on fresh literals on top. Then state that freshness is a property of the literal expression that widening or an assertion destroys, and finish with the motivation — it exists to catch misspelled and obsolete keys at the one place the compiler can be confident they are useless.

  • Does the same check fire on a literal returned from a function with a declared return type?
    Yes. Any position where a literal meets a contextual type is checked: annotated variable declarations, call arguments, `return` expressions against a declared return type, elements of an annotated array literal, and object literals nested inside another fresh literal. It is contextual typing that triggers it, not the `=` sign.
  • Why did the designers let freshness disappear the moment the value is in a variable?
    Because the justification for the check evaporates. In a fresh literal, an undeclared property is unreachable through the target type and therefore almost certainly a mistake. Once the value is bound to a wider-typed variable, other code may legitimately read that property, so rejecting the assignment would break correct programs.
  • If the check is only about literals, does the extra property still exist after compilation?
    Yes. The type layer is erased and TypeScript never rewrites your object, so the emitted JavaScript carries the extra key exactly as written. Anything that walks the object at run time — serialisation, key enumeration, a database driver — will see it.

saying these in an interview costs you the question

  • Says structural typing forbids extra properties in general
  • Claims TypeScript removes the extra property from the object
  • Cannot explain why the intermediate-variable version compiles
  • Recommends `as` as the correct fix for the error
  • Describes the check as a runtime validation of the object

context