Structural typing, intersections and extra properties
Why a value with more properties than its type fits anyway, when excess property checks fire, and how intersections combine shapes.
After this lesson you can
- Predict whether a value is assignable by comparing shapes, not names
- Say exactly when an excess property check applies
- Combine shapes with an intersection and know what conflicting members do
- Strip extra properties deliberately before data leaves the system
TypeScript's type system is structural: a value fits a type when it has the properties the type asks for, with compatible types. The name of the type, and any properties beyond the ones asked for, do not matter.
interface Named { name: string }
class Product { constructor(public name: string, public price: number) {} }
const n: Named = new Product("Desk", 120); // fine: it has a name
Product never mentions Named, and it does not need to. Languages such as
C# and Java are nominal: there, a class fits an interface only if it
says it implements it.
Extra properties are still there
A type describes what you may use, not what the object contains.
An object with more properties than its type keeps them all at runtime, and
anything that walks the whole object — JSON.stringify, spread,
Object.keys — sees them:
interface PublicUser { id: number; name: string;} function toResponse(user: PublicUser): string { return JSON.stringify(user);} export function run(): string { const row = { id: 7, name: "Nino", passwordHash: "x9$f..." }; // Not a fresh literal, so no excess-property check: row has at least // id and name, which is all PublicUser asks for. return toResponse(row);}This is a real source of data leaks: a database row typed as a smaller public shape is serialised straight into a response, password hash included. If only some fields may leave, copy those fields into a new object.
When excess property checks fire
The compiler does flag extra properties in one situation: an object
literal written directly where a type is expected (assigned to an
annotated variable, passed as an argument, returned from a function with a
declared return type). There a misspelt colour for color is far more
likely than a deliberate extra, so it is an error. The same object stored
in a variable first is not "fresh" any more, and the check does not run.
greet({ name: "Nino", admin: true }); // error: 'admin' does not exist
const u = { name: "Nino", admin: true };
greet(u); // fine
Intersections
A & B is a value that is both: it has every property of A and every
property of B. It is how you add a set of fields to any shape:
type Audited = { createdBy: string; createdAt: string };type Note = { title: string; body: string }; // An intersection has every property of both sides.type AuditedNote = Note & Audited; function audit<T extends object>(item: T, by: string): T & Audited { return { ...item, createdBy: by, createdAt: "2024-03-01" };} export function run(): AuditedNote { const note: Note = { title: "Standup", body: "Ship the fix" }; return audit(note, "nino");}When both sides declare the same property with incompatible types, that
property's type becomes never ({ id: string } & { id: number } has an
id no value can satisfy). interface B extends A reports that conflict
as an error where you declare it, which is one reason to prefer extends
for building object types.
Common mistakes
- Serialising a wider object through a narrower type. As in the first
example, the narrow type hides
passwordHashfrom the compiler, not fromJSON.stringify. - Removing fields with a rest spread.
const { passwordHash, ...rest } = usersends everything else, including any field added to the table next month. Copy the allowed fields explicitly instead. - Thinking excess property checks protect you everywhere. They only apply to fresh literals; values from variables, functions or JSON pass with their extras.
- Expecting
&to mean "either". An intersection of two object types has the properties of both; "either" is a union,|.
What interviewers ask
- Is TypeScript's type system nominal or structural, and what follows? Structural: anything with the required members is assignable, so two unrelated types with the same shape are interchangeable.
- When does TypeScript complain about extra properties? Only for a fresh object literal assigned or passed to a typed target; not for a variable.
Exercise · Pro
Only the public fields leave
Exercises are free in the first 3 lessons of every course. From here on they are part of Pro and Sprint; the lesson and its runnable examples stay open.
Upgrade