"Structural typing lets too much through. Excess property checking quietly puts the safety back at one specific spot."
The surprise that catches typos
You learned that structural typing accepts extras. So this should compile:
interface Box { width: number }
function take(b: Box) {}
take({ width: 100, height: 200 }); // 'height' is extra — shape still matches
Except it doesn't. The compiler complains: "Object literal may only specify known properties, and 'height' does not exist in type 'Box'." This is excess property checking, a special-cased rule that only fires when you pass an object literal directly.
Why this rule exists
Pure structural typing has a problem: when you write a literal like { widht: 100 } (typo!), the literal happens to be a valid shape for some interface — just an empty one or a different one. Structural rules would accept that as a wider type. The bug would slip through.
Excess property checking solves this. When an object literal is assigned/passed directly to a typed slot, the compiler additionally checks that no unknown keys appear. Typos get caught. Extras get rejected. The cost: you can't blindly pass extras to a function that types its parameter.
Why it only fires for literals
If you assign the same object via a variable first, structural typing applies normally — no excess check.
const extras = { width: 100, height: 200 };
take(extras); // ✅ structural — accepted
The reasoning: a literal is fresh code where typos are likely. A variable already exists for some other reason — the extras probably mean something the literal-time you didn't think to put in the interface.
Escape hatches when you legitimately want extras
Three common patterns to bypass excess checking when you mean it:
- Store in a variable first. The clearest signal that you know about the extras.
- Add an index signature like
[key: string]: unknownto the interface. This says "extras are explicitly allowed." - Cast with
as:take({ width: 100, height: 200 } as Box). Use rarely — it's the least safe.
as.Pippa's confession
as to bypass it, each one had a comment explaining the intent. The default position is "the compiler is right."