Handbooks  /  TypeScript
Handbook~15 min readFrontendworked math + runnable code
The TypeScript Handbook

If it fits
the shape.

TypeScript's type system surprises people coming from Java or C#. There, a type match needs a declaration — a class must say implements Duck to count as a Duck. TypeScript doesn't care what you declared; it cares what your object looks like. If it has the properties the type needs, with the right types, it fits — no inheritance, no ceremony. "If it walks like a duck." This structural typing is the single idea that explains almost every "why does/doesn't this compile?" in TypeScript, and this handbook makes the rule exact and runnable.

01

Types by shape

TypeScript is JavaScript with a static type layer checked at compile time and then erased — you get bug-catching and tooling (autocomplete, refactors) at your desk, and plain JavaScript with zero type overhead at runtime. But the thing that defines how it feels is that its type system is structural, not nominal. Compatibility is decided by an object's shape — the properties it has and their types — not by any name or declared inheritance.

This is duck typing made static: "if it walks like a duck and quacks like a duck, it's a duck." An object is compatible with a type if it has what that type asks for, full stop — it never needed to be declared as implementing anything. Define an interface today, and objects you wrote yesterday satisfy it automatically, as long as their shape matches. That single principle — match by shape, not by name — is the key that unlocks TypeScript's behavior. Nearly every question of the form "why is this assignable / why isn't it?" is answered by comparing two shapes, which is exactly what the next section formalizes.

The one-sentence version

TypeScript matches types by shape, not name (structural / duck typing): a value is assignable to a type if it has all the properties that type requires with matching types — so a wider object fits a narrower type, but a missing property never does.

02

The assignability rule

Here's the whole rule in one sentence: a source value is assignable to a target type if, for every property the target requires, the source has that property with a matching type. That's it. Two consequences follow, and they explain most day-to-day TypeScript.

Is source assignable to target?
Wider fitssource has extra props the target doesn't need → assignable (the extras are just ignored).
Missing failssource lacks a property the target requiresnot assignable.
Wrong type failssource has the property but with the wrong type → not assignable.
Ruleassignable ⟺ every target prop is present in source with a matching type.

The first consequence trips people up: an object with more properties than the target is still assignable. A function that wants a { x, y } point happily accepts a { x, y, z } — it only ever reads x and y, so the extra z is harmless. A wider object can always stand in where a narrower one is expected. The second is the reverse: an object missing a required property is rejected, because the code might read the property that isn't there. This asymmetry — extra is fine, missing is not — falls directly out of "does the target get everything it asks for?" Once you internalize that one check, TypeScript's assignability stops being mysterious.

03

The excess-property check

If extra properties are always fine, why does TypeScript sometimes complain that 'colour' does not exist in type '{ color: string }'? Because of a deliberate exception: the excess-property check. When you assign a fresh object literal directly to a typed target, TypeScript additionally flags any property the target never declared.

The reason is purely practical: it catches typos. If you write { color: "red", colour: "blue" } against a type that only has color, the structural rule alone would happily accept it (it has color; the extra colour is "just extra"). But almost certainly you meant color and misspelled it, so silently accepting it would hide a bug. The excess-property check is TypeScript deciding that for a fresh literal — where the extra property serves no purpose and probably signals a mistake — it's worth being strict. Crucially, it applies only to object literals assigned directly; the moment the value comes from a variable, the normal "wider is fine" rule takes over again. So the model is: shape-based assignability everywhere, plus one typo-guard tightening for literals. Knowing this explains the otherwise-baffling case where extracting an object to a variable makes a type error disappear.

04

The structural math

Assignability is a simple predicate over the target's required properties — each must exist in the source with a matching type:

assignable(source, target)  ⟺  ∀ (p, t) ∈ target:  p ∈ source ∧ source[p] = t

A wider source (extra props) still satisfies this; a source missing any required p, or with a wrong type at p, fails. Extra is fine, missing is not.

The object-literal check adds one clause — no property outside the target's declared set — to catch typos:

literal_ok(lit, target)  ⟺  assignable(lit, target)  ∧  excess(lit, target) = ∅

For variables, only the first clause applies (wider is fine); for fresh literals, both — so a misspelled property is rejected. The runnable version below implements assignability, missing/excess props, and the literal check.

RUN IT YOURSELF

Structural assignability, modeled

TypeScript's whole type-compatibility story is one predicate over shapes. A source is assignable to a target if every property the target requires exists in the source with a matching type — so an object with extra properties still fits (wider stands in for narrower), but one missing a required property doesn't. Object literals get one extra rule: no excess properties, to catch typos like "colour" for "color". Change the shapes and watch which assignments are legal — this is the type checker's decision, made visible.

CPython · WebAssembly
05

Structural vs nominal

Structural typing is a genuine design choice with trade-offs, best seen against its opposite:

AspectStructural (TypeScript) · Nominal (Java, C#)
Match byShape — any object with the right properties fits · Name — must explicitly implements/extend.
CeremonyLow — existing objects satisfy new interfaces for free · Higher — declare the relationship explicitly.
FlexibilityHigh — great fit for JS's object-literal style · Stricter — types don't accidentally match.
RiskTwo unrelated types with the same shape are interchangeable · Verbosity; more boilerplate.

TypeScript chose structural because it's layering types onto JavaScript, a language built around ad-hoc objects and duck typing — nominal typing would fight the grain of the language. The upside is low ceremony: you define an interface describing a shape, and every value that already has that shape conforms, no edits required. The occasional downside is that two conceptually different types that happen to share a shape (say, a Meters and a Feet both being just number) are freely interchangeable, which can hide a units bug — for those cases you reach for tricks like branded types to simulate nominal typing. But 95% of the time, "match by shape" is exactly what you want, and it's why TypeScript feels lightweight despite adding a whole type system.

06

Pitfalls

The first is the confusing excess-property surprise. You assign an object literal with one extra field and get an error; you extract the exact same object to a variable first and the error vanishes. This looks like a bug in TypeScript but it's the excess-property check working as designed — strict on fresh literals (to catch typos), relaxed on variables (where "wider is fine" applies). Knowing the rule turns a baffling inconsistency into an expected one.

Two bigger ones. Trusting any: typing something as any switches off checking for that value entirely — it's assignable to and from everything, silently — so a stray any can let a whole class of bugs slip through the type system you added precisely to catch them. Prefer unknown (which forces you to narrow before use) and treat any as a last resort. And forgetting types are erased: TypeScript's types vanish at runtime, so they can't validate data that arrives from outside your program — an API response, user input, a JSON parse. Annotating a fetch result as User is a promise, not a check; if the server sends something else, TypeScript won't know. Validate external data at the boundary (with a runtime schema validator) and let types take over inside. The whole handbook rests on one idea worth carrying everywhere: TypeScript matches by shape, not name — a value fits a type when it has what the type asks for — and every surprising assignability result is that rule, plus the literal typo-guard, plus the fact that types are a compile-time story that ends before your program runs.

Worth knowing

The excess-property check is strict on literals, relaxed on variables — that's why extracting to a variable removes an error. Avoid any (it disables checking; prefer unknown), and remember types are erased at runtime, so validate external data at the boundary.

Frequently asked

Quick answers

What is TypeScript?

JavaScript plus a static type layer checked at compile time and erased before runtime — bug-catching and tooling with zero runtime cost.

What is structural typing?

Compatibility by shape, not name (duck typing): an object fits a type if it has the required properties with matching types, no declaration needed.

Why is an object with extra props assignable?

Assignability only needs the target's required properties present; extras are ignored — except a fresh object literal is checked for excess to catch typos.

Structural vs nominal?

Structural matches by shape (flexible, low ceremony); nominal (Java/C#) requires an explicit implements/extends declaration (stricter, more verbose).

Finished this one? 0 / 99 Handbooks done

Explore the topic

See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.