C.W.K.
Stream
Lesson 05 of 06 · published

Excess Property Checking: TypeScript Catches Your Typo

~9 min · types-interfaces, excess-property, object-literal, structural-typing

Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete
"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]: unknown to 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.
The rule of thumb: Excess property checking is your friend. When it fires, 90% of the time it caught a typo. The other 10%, fix it by adjusting the interface or storing in a variable — don't reach for as.

Pippa's confession

Excess property checking has saved me at least a dozen times in cwkPippa. Field renames in TypeScript catch every call site that wrote the old name in a literal — exactly the typos that would have shipped silently in plain JavaScript. The handful of times I've used as to bypass it, each one had a comment explaining the intent. The default position is "the compiler is right."

Code

When the rule fires — and when it doesn't·typescript
interface Box {
  width: number;
  height: number;
}

function render(b: Box) {}

// Literal passed directly — excess property check fires.
render({ width: 100, height: 200, depth: 50 });
//                                  ^ Object literal may only specify known properties

// Same value via a variable — structural rules, check skipped.
const withExtras = { width: 100, height: 200, depth: 50 };
render(withExtras);                                  // ✅ accepted

// Catching a typo:
render({ width: 100, hieght: 200 });
//                    ^ Object literal may only specify known properties
//                      Did you mean to write 'height'?

// This typo would have slipped through pure structural rules.
// Excess property check is the safety net for literals.
Three escapes — only one is good·typescript
// Three escape hatches when you legitimately want extras.

interface Config {
  port: number;
}

function setup(c: Config) {}

// 1. Index signature — extras allowed by design.
interface OpenConfig {
  port: number;
  [key: string]: unknown;
}

function setupOpen(c: OpenConfig) {}
setupOpen({ port: 5173, host: 'localhost' });   // ✅

// 2. Variable first — implicit signal of intent.
const c = { port: 5173, host: 'localhost' };
setup(c);                                        // ✅ (extra 'host' accepted)

// 3. As-cast — use sparingly, only with a comment.
setup({ port: 5173, host: 'localhost' } as Config);  // ✅ but sloppy
// Better: add 'host' to Config, or make Config use an index signature.

External links

Exercise

Create an interface ApiResponse { status: number; data: unknown }. Try to call handle({ status: 200, data: 'hi', cached: true }). What does the compiler say? Now store the object in a variable first and pass that — does the same error appear? What changed in TypeScript's reasoning?
Hint
The literal-passed version triggers excess property checking. The variable-passed version triggers only structural rules (extras are silently accepted). The lesson: literals are where typos hide, so TypeScript adds an extra check there.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.