Structural typing
TypeScript never asks what a type is called. It compares the members a value actually has against the members the target requires. This chapter explains that rule, and then takes apart the one exception that confuses almost every beginner: the excess property check.
The duck test: shape decides, not the name
A milk tea shop is hiring. The owner does not read diplomas. The only question is whether you can make tea. TypeScript judges types the same way.
An interview with no paperwork
The shop needs staff. The posting says: has a name, can make tea. Someone shows up with no certificate at all. The owner does not care. Name? Yes. Make tea? Made one on the spot. Hired. Whether that person used to be called a "full-time employee" or "the shop next door's staff" is never asked.
That is the whole idea of structural typing. Whether a value belongs to a type depends on which members the value has, not on what the value ever declared itself to be. The same idea has an older name: the duck test. If it walks like a duck and quacks like a duck, treat it as a duck.
Staff. The compiler compared the members of zhen with the members Staff requires, found all of them, and allowed the call.Staff is a label you attached to a shape so that people can read the code. When the compiler compares types, the label is not part of the comparison.
Which members exist, and the type of each member. That list is what the compiler treats as the type. Two types with the same list are two names for one thing.
Every member the target requires is present, and each one has a compatible type. The check passes. No declaration is needed, because compatibility is computed rather than registered.
Common mistake: “A new interface name means a new type”
It does not. interface A { x: number } and interface B { x: number } are assignable to each other in both directions. The name is a label, and the shapes are the same. type behaves exactly like interface here: neither one creates a separate type just by having a separate name. If you need two same-shaped types to stay apart, §05 shows the standard way.
Two designs: compare declarations, or compare members
Java and C# take the other road. They use nominal typing, and they give the opposite answer for the same code.
A nominal type system compares declarations. Two types are related only if one of them says so, with extends or implements. A structural type system compares members. Nothing has to be declared. Look at the right-hand example again: in TypeScript, even classes are compared by shape. That is the rule developers coming from Java run into first. There is exactly one exception, and it needs a private or protected member to appear. Chapter 08 covers it.
Why TypeScript chose structural typing
Because its job is to describe JavaScript that already exists. Think about what ordinary JavaScript values look like: an object literal written on the spot, a result returned by JSON.parse, an object assembled from a few functions. None of those declared anything. If compatibility required a declaration, every existing JavaScript file would have to be rewritten before TypeScript could type it, and nobody would do that. Structural typing lets TypeScript describe JavaScript as it is written, which is what makes the "superset of JavaScript" claim possible.
Direction: more members can stand in for fewer
The duck test has a direction. Someone with twenty skills can do a job that asks for three. The reverse does not work.
The set view: more members, smaller set
One more way to see it, and it is the one that stays reliable: a type is a set of values. { name: string } describes a large set, because every object with a name belongs to it. { name: string; makeTea: () => void; years: number } describes a much smaller set, because more requirements means fewer values qualify.
So "more members" means "more specific", which means "smaller set". Every value in the smaller set is also in the larger one, so a Barista can always be used where a Staff is expected. The reverse is not true. This direction is the same one that unions in chapter 03 and generic constraints in chapter 05 depend on.
Optional member vs a member that may be undefined
These two look similar and are not the same shape. { note?: string } says the key may be missing. { note: string | undefined } says the key must be present, and its value may be undefined. Because the second one requires a member that the first one does not, the first is not assignable to the second.
Members that are functions follow the same shape comparison, but their parameters are checked in the opposite direction from their return types. Chapter 02 covers that in full, including the strictFunctionTypes flag and the exception for method syntax.
The excess property check: object literals are treated differently
§03 just said extra members are fine. Now the compiler appears to say the opposite. This is the point where most beginners get stuck, so this section takes it apart.
The object is identical in both cases. Written at the call site it is an error. Stored in a variable first it compiles. This is not a bug. It is a separate, stricter check called the excess property check. It runs only on a fresh object literal, which means a literal written directly where a type is expected. It is deliberately not part of the assignability rule, which is exactly why storing the object first makes it disappear. The reason for the design is the difference between these two situations:
This object was written on the spot and handed over immediately. It has no second use. So an unexpected property can only mean one of two things: a typo (sweetnes), or a misunderstanding of the type. Both are bugs, so the compiler reports it. That is how the half sugar on the left was saved.
An object held in a variable may be used elsewhere for a perfectly valid reason. It might really be a richer Barista that is also being used as Staff. §03 allows that, so the compiler allows it. The cost is that a misspelled property passes through unnoticed, which is how the half sugar on the right was lost.
startShift as a literal. The members are now checked one by one.Silencing the check with as does not fix anything
makeOrder({ item: "Boba milk tea", sweetnes: "half sugar" } as Order) does make the error go away. The typo is still there, and the half sugar is still lost. as tells the compiler to stop checking; it does not change the object. The fix is always the same: read the property name in the error message and correct the spelling.
How to read the two error messages
When the extra property looks like a misspelling of a real one, TypeScript names the fix: Object literal may only specify known properties, but 'sweetnes' does not exist in type 'Order'. Did you mean to write 'sweetness'? ts(2561)
When the extra property resembles nothing in the target, there is no suggestion and the code is different: Object literal may only specify known properties, and 'cup' does not exist in type 'Order'. ts(2353)
Both say the same thing: an object literal may only contain properties the target type knows about. Seeing ts(2561) is good news, because the compiler already worked out the correct spelling for you.
When identical shapes are a problem
Two types that mean completely different things can be swapped freely, as long as their members happen to line up.
A pickup order got a courier label
The shop has two types: DeliveryAddress for the delivery platform, and PickupInfo for orders collected in store. Two people defined them separately, and both ended up as a phone number plus a note. One day someone passed a pickup order to the function that prints courier labels. The compiler said nothing, because the members lined up and it had no reason to object. That evening a courier followed the note on the label and went to the shop to collect a delivery that did not exist.
The version you will meet more often is mixed-up identifiers. UserId and PostId are both string, so looking up a user by a post id compiles without a word. There is only one real fix: write the difference into the shape. If only shapes are compared, then make the shapes differ.
__brand exists only at compile time and costs nothing at run time. Be honest about the price: a plain string is not assignable to UserId, so creating one always needs an assertion. The usual discipline is to write a single toUserId(s: string) function that performs that assertion, call it only where data enters the system, and never write as UserId anywhere else. Lab 3 in §06 walks through it.One more trap: the empty object type {}
Take the duck test to its limit. {} requires zero members, and almost every value satisfies a list of zero requirements. 42, "tea", true, a function, an array, and any object are all assignable to {}. Only null and undefined are rejected, and only because strictNullChecks is on. So write object when you mean any object, and unknown when you mean any value at all and you will narrow it before use. {} looks like a requirement and is not one.
Practice
Four tasks, all of which run in the TypeScript Playground (typescriptlang.org/play): trigger both faces of the excess property check, then reproduce a same-shape accident and block it.
Chapter quiz
Eight questions. After this chapter you should be able to answer “how does TypeScript decide that two types are compatible” from shapes, to sets, to the special treatment of object literals.
When TypeScript decides whether two types are compatible, what does it compare?
Given interface A { x: number } and interface B { x: number }, and const a: A = { x: 1 }, what happens on const b: B = a?
type Staff = { name: string }, and the variable barista has type { name: string; makeTea: () => void }. Which assignment compiles?
makeOrder({ item: "Milk tea", cup: "large" }) fails because of the extra cup, but const d = { item: "Milk tea", cup: "large" } followed by makeOrder(d) compiles. Why?
Which of these statements about structural typing are true? (multiple answers)
UserId and PostId are both declared as string, so passing a post id where a user id is expected compiles. What is the right fix?
A type system that decides compatibility by shape rather than by name is called ________ typing (answer in English).
You want a parameter type that means "any object is fine, but not a primitive value". Which one do you write?
- TypeScript uses structural typing: compatibility depends on the members, not on the name. The name is a label; the member list is the type. Classes are compared the same way.
- Direction matters: a type with more members is assignable to one with fewer, never the other way round. The set view is the easiest way to remember it. More requirements means a smaller set, and a smaller set is contained in the larger one.
- The excess property check applies only to a fresh object literal: written at the call site it is an error, stored in a variable first it is allowed. It is an extra check layered on top of assignability, aimed at typos, not a general rule.
- Silencing that error with
ashides the typo instead of fixing it. Read the property name in the message and correct the spelling. ts(2561) even tells you the correct name; ts(2353) is the same error without a suggestion. - Identical shapes are interchangeable even when they mean different things. For accidents like
UserIdmixed withPostId, use a branded type to write the difference into the shape. And note that{ a?: number }is not{ a: number | undefined }, and that{}requires nothing at all: writeobjectwhen you mean any object.