AcademyTypeScript
Structural typing: shapes, not names
TypeScript compares shapes, not names. That is why an unrelated object type-checks, why excess properties sometimes error, and how to make a type nothing else can satisfy.
3 min readUpdated 3 exercises
Names do not matter, shapes do
In Java or C#, a type is satisfied by the classes that declare they implement it. TypeScript asks a different question: does this value have the members the type requires?
interface Point { x: number; y: number }
class Vector {
constructor(public x: number, public y: number) {}
}
const p: Point = new Vector(1, 2); // fine — Vector never mentions Point
const q: Point = { x: 1, y: 2 }; // also fineNothing declared a relationship. The shapes match, so the assignment is allowed. This is why implements is almost decorative in TypeScript: it checks the class against the interface at the declaration, but it is not what makes the class assignable later.
It is also why the type you wrote is usually wider than the thing you meant.
type UserId = string;
type OrderId = string;
function cancel(order: OrderId) { /* ... */ }
const userId: UserId = 'u_42';
cancel(userId); // compiles. Both are string.Two names, one type. The compiler sees string twice and has nothing to complain about.
Excess properties, and the exception that confuses everyone
If shapes only need to match, why does this error?
interface Options { timeout: number }
const opts: Options = { timeout: 50, retries: 3 };
// Error: Object literal may only specify known properties,
// and 'retries' does not exist in type 'Options'.Because of a special rule that applies to object literals assigned directly, and only to them. It exists to catch typos, and it disappears the moment the value goes through a variable:
const raw = { timeout: 50, retries: 3 };
const opts: Options = raw; // no error — raw is not a fresh literalBoth are structurally fine. The first one is the compiler doing you a favour; the second is the type system's actual rule. Knowing which is which saves an hour the first time a config object stops erroring after you extract it to a variable.
When you need a type nothing else satisfies
Structural typing has no way to say "this string is different from that string" — so you add something to the shape that nobody can produce by accident. That is a branded type:
type UserId = string & { readonly __brand: 'UserId' };
type OrderId = string & { readonly __brand: 'OrderId' };
function toUserId(value: string): UserId {
// The one cast in the codebase, in the one function allowed to make one.
return value as UserId;
}
cancel(toUserId('u_42')); // Error: UserId is not assignable to OrderIdThe brand exists only in the type system — at runtime it is still a string, with no wrapper and no cost. What it buys is that the only way to get a UserId is to go through the function that validates one, which is the whole of "parse, don't validate" in three lines.
| Problem | Structural answer |
|---|---|
| Two ids of the same primitive type | brand them |
| A typo'd property in a config | rely on the excess property check — keep the literal inline |
| A class that must match an interface | implements, for the error at the declaration |
| A value from outside the program | unknown, then narrow |
The habit this should change
Stop reading a type name as a guarantee. function send(user: User) accepts anything with the right members — including objects from a different module that happen to line up, and including the object you built by spreading three others. If that matters for a particular value, make the type structurally unique. If it does not, structural typing is doing exactly what it is for: letting your code work with data it was not introduced to.
Now practice it
2 challenges · 1 quizReading this page is the cheap half. These are the exercises that make you use it.
- Branded Types — Parse-Don't-ValidateProBuilds the branded type and the one function allowed to mint it, which is the pattern this page ends on.ChallengeSolve · MediumChallengeSolve · Medium
- AdapterProAn adapter is trivial in a structural system, and writing one shows exactly why names never enter the check.ChallengeSolve · EasyChallengeSolve · Easy
- Type System · Type System BasicsType system questions about assignability, which is the rule this page describes under another name.QuizTake · EasyQuizTake · Easy
Read next
Type aliases vs interfaces
What the two actually differ on — declaration merging, unions, and the error messages you get back — and the rule that settles it in a real codebase.
3 min
Type assertions: as vs satisfies
What a type assertion actually does to your program, the two places it is defensible, and why satisfies gives you the check without losing the type.
3 min