Structural typing has a stricter cousin: pass an object literal directly where an interface is expected, and TypeScript's excess property check flags any property the interface doesn't declare — even though the exact same object assigned to a variable first would pass with no complaint. Confirmed by compiling both forms side by side.
1. What does marking an interface property with `?` (like `email?: string`) mean?
Interview Questions
2 questions
Cheatsheet
Download-ready reference
Cheatsheet
interface User {
id: number;
name: string;
email?: string; // OPTIONAL — may be omitted entirely
readonly createdAt: Date; // cannot be REASSIGNED (shallow only!)
}
EXTENSION: interface Dog extends Animal, Pet { ... }
→ requires the UNION of everything all extended interfaces declare
⚠️ EXCESS PROPERTY CHECK — literals get STRICTER scrutiny than
normal structural typing:
const x = { a: 1, extra: true }; fn(x); // ✅ fine (variable)
fn({ a: 1, extra: true }); // ❌ ERROR (literal)
Workaround: assign to a variable first, OR use `as` assertion —
only do this when the extra properties are GENUINELY intentional.
DECLARATION MERGING (interface-ONLY capability):
interface Box { width: number }
interface Box { height: number } // MERGES → Box needs BOTH
⚠️ type aliases CANNOT do this — see Type Aliases
readonly is SHALLOW:
c.settings = {...} // ❌ blocked (reassigning the property)
c.settings.theme = "x" // ✅ allowed (mutating something INSIDE it)
The beginner framing: an interface describes the shape a value must have — which properties, and what type each one is — giving both a compile-time contract and, in most editors, real autocomplete for anything typed with it.
interface User { id: number; name: string; email?: string; // optional — may be omitted entirely readonly createdAt: Date; // cannot be reassigned after creation}const user: User = { id: 1, name: "Ada", createdAt: new Date() }; // email omitted — fineuser.createdAt = new Date(); // ❌ error — readonly
Confirmed by compilation: email being marked ? means an object satisfying User may omit it entirely (or set it to undefined) with no error, while readonly on createdAt compiles cleanly on initial assignment but produces Cannot assign to 'createdAt' because it is a read-only property on any later reassignment.
Extension: building interfaces from other interfaces#
interface Animal { name: string }interface Dog extends Animal { breed: string;}const rex: Dog = { name: "Rex", breed: "Labrador" }; // must satisfy BOTH
extends composes interfaces — Dog requires everything Animal requires, plus its own additional properties. An interface can extend multiple others (interface Dog extends Animal, Pet { ... }), requiring the union of everything all of them declare.
The excess property check: structural typing's stricter cousin#
This is the exception TypeScript Basics flagged as "covered in Interfaces" — and it's the single most common source of "wait, why did this suddenly error?" confusion for people who've internalized structural typing's normal permissiveness.
interface Point { x: number; y: number }function logPoint(p: Point) { console.log(p.x, p.y); }const location = { x: 1, y: 2, z: 3 }; // assigned to a variable FIRSTlogPoint(location); // ✅ compiles fine — normal structural typing, extra property ignoredlogPoint({ x: 1
Confirmed by compiling both forms: the exact same shape is accepted when assigned to a variable first, but rejected when passed as a literal directly at the call site. This isn't a contradiction of structural typing — it's a deliberate, additional safety net specifically for object literals, since a literal's excess property is very often a typo (nmae instead of name) that structural typing alone wouldn't catch, since the "extra" misspelled property would just be silently ignored otherwise.
Declaration merging: a genuine interface-only capability#
interface Box { width: number }interface Box { height: number } // SAME name — this MERGES with the declaration aboveconst b: Box = { width: 10, height: 20 }; // Box now requires BOTH width AND height
Confirmed by compilation: two interface declarations with the same name don't conflict — they merge into a single interface requiring the union of both declarations' properties. This is a genuine interface-specific capability that type aliases cannot do (attempting the same with two type Box = {...} declarations is a compile error) — covered in full in Type Aliases.
interface Settings { theme: string }function applySettings(settings: Settings) { console.log(settings.theme);}const userPrefs = { theme: "dark", fontSize: 14 };applySettings(userPrefs); // does this compile?applySettings({ theme: "dark", fontSize: 14 }); // does THIS compile?
Solution
The first call compiles fine — userPrefs is a variable, so normal structural typing applies: it has the required theme: string, and the extra fontSize is simply irrelevant. The second call fails to compile — passing the object literal directly triggers the excess property check, and fontSize isn't a known property of Settings, producing a compile error. Same shape, same values, different outcome purely based on whether the object was a literal at the call site or a variable passed by reference.
Build a function that demonstrates working around the excess property check deliberately, when the extra properties are genuinely intended:
interface ApiResponse { status: number; body: string }function logResponse(res: ApiResponse) { console.log(res.status, res.body);}// Option 1: assign to a variable first (bypasses the excess property check entirely)const response = { status: 200, body: "OK", headers: {} };logResponse(response); // ✅ fine — not a literal at the call site
Both approaches sidestep the excess property check — the first because it's no longer a literal by the time it's passed, the second because an assertion explicitly tells the compiler to trust the shape. Neither approach makes the extra headers property accessible through the ApiResponse-typed parameter inside logResponse — the check is only about what's ALLOWED to be passed in, not about hiding or removing the extra data at runtime.
The excess property check is a targeted exception layered on top of the general structural typing model from TypeScript Basics — understanding both together (normal structural permissiveness PLUS this literal-specific tightening) gives the complete, accurate picture rather than either half alone. Declaration merging's interface-only nature is exactly what Type Aliases covers as one of the concrete, non-folklore differences between the two — not a stylistic preference, but a genuine capability gap.
1. Being surprised by an excess property error after structural typing "should" allow it#
logResponse({ status: 200, body: "OK", extra: true }); // ❌ "why is this erroring??"
This confusion usually comes from only having learned the general structural typing rule — the excess property check is a real, separate exception specifically for literals, not a contradiction of what was learned earlier.
2. Forgetting that readonly only prevents reassignment, not mutation of nested objects#
readonly is shallow — it only protects the property it's directly applied to, not anything nested inside that property's value.
3. Assuming an accidental duplicate interface declaration is always a merge you wanted#
interface User { id: number } // in file A// ... later, in the SAME scope, someone accidentally writes:interface User { id: string } // ❌ error — conflicting property type, not silently overridden
Declaration merging only works cleanly when the duplicate declarations don't directly conflict on a shared property's type — an accidental redeclaration with an incompatible type produces a real error, not silent replacement.
Use readonly for properties that genuinely shouldn't change after construction (IDs, creation timestamps) — it's a real, compiler-enforced guarantee, not just a naming convention.
Don't fight the excess property check by habitually assigning to a variable first just to avoid it — if it's flagging a genuine typo, that's exactly the safety net working as intended; only bypass it when the extra properties are deliberate.
Reach for interface extension (extends) when building up related, hierarchical shapes — it reads clearly and composes well with multiple interfaces.
Remember declaration merging is interface-specific — don't rely on it as a general pattern if the code might later need to become a type alias for other reasons (unions, mapped types), since that capability wouldn't carry over.
The excess property check and readonly enforcement are both purely compile-time — neither adds any runtime cost; the compiled JavaScript is identical whether or not these checks catch something at build time.
Declaration merging doesn't create any actual runtime overhead either — by the time code runs, the merged interface has done its job entirely at the type-checking stage and has already been erased.
, y:
2
, z:
3
});
// ❌ ERROR — object LITERAL passed DIRECTLY
// "Object literal may only specify known properties, and 'z' does not exist in type 'Point'"
// Option 2: a type assertion, when you're SURE the extra properties are intentional
logResponse({ status: 200, body: "OK", headers: {} } as ApiResponse); // ✅ also fine