"If it walks like a duck and types like a duck — TypeScript says yes, it's a duck."
The single most distinctive thing about TypeScript
Most static type systems are nominal: two types with identical shapes but different declared names are incompatible. Java's class Pippa and class Ttori with identical fields cannot be assigned to each other unless one extends the other. The compiler enforces the family tree.
TypeScript is structural: two types are compatible if their shapes match, regardless of name or declared lineage. type Pippa = { name: string } and type Ttori = { name: string } are compatible — you can pass a Pippa where a Ttori is expected. The compiler only cares about the shape, not the label.
Why this matters
Structural typing is how TypeScript stays light over JavaScript. JavaScript was always duck-typed at runtime — if (obj.quack) obj.quack() doesn't care what class obj came from. TypeScript moved that duck-typing to compile time without forcing every object to declare its lineage. The result feels JavaScript-natural and catches bugs without bureaucracy.
It also enables patterns that nominal systems struggle with. Want to accept any object that has a name and email field? You don't need an interface — you can say (u: { name: string; email: string }). Want a function that works on anything with .length? (x: { length: number }). The shape is the type.
The compatibility rule
A value of type A is assignable to type B if A has at least every member of B (with compatible types). Extras are fine — fewer members are not.
type Has1 = { a: number };
type Has2 = { a: number; b: string };
const v2: Has2 = { a: 1, b: 'x' };
const v1: Has1 = v2; // ✅ Has2 has everything Has1 needs (and more)
This is why a class doesn't need implements to satisfy an interface. If the shapes line up, the compiler accepts it.
Has2 = Has1 + extras, then Has2 → Has1 assignment is fine; Has1 → Has2 is not. Wider types accept narrower ones, never the other way around.One caveat to "extras are fine": it holds when the value arrives through a variable. Pass an object literal directly to a function call or assignment and TypeScript layers on a stricter rule — excess property checking — that rejects unknown properties to catch typos. greetAnyone({ name: 'Pippa', age: 21 }) errors, but greetAnyone(pippa) through a variable does not. That's the very next lesson.
The branded-types pattern (escape hatch for nominal)
Sometimes you need nominal — you want UserId and PostId to be incompatible even though they're both number. The community pattern is brands: type UserId = number & { __brand: 'UserId' }. The intersection with a unique marker makes the types structurally distinct. There's no runtime cost — the brand is type-layer only.
Pippa's confession
BrainName is just a string literal union, not a class. Conversation shapes flow between components by structure. The escape hatch — branded types — shows up in exactly three places where we needed ChatId and CouncilId to be incompatible despite both being strings. The default is structural; the brand is the rare case.