skip to content

Assignability and Duck Typing

A value is assignable to a type when it has at least the required members with compatible types — no `implements` needed, and extra members are fine. Interviewers ask this to check you can predict, member by member, why two unrelated types are silently interchangeable.

part ofTypeScriptoverview, primer and where to startread it →
on this pageshow

questions

5

TypeScript checks type compatibility structurally rather than nominally. What does that mean for a function whose parameter is typed `interface Point { x: number; y: number }` — which values will the compiler accept?

level: juniorimportance: must knowfreq 78%

answer

  1. names don't decide compatibility
  2. compare member by member
  3. source may have more, never less
  4. implements is optional, not enabling

basics

~20 s

TypeScript compares types by shape, not by declared name. Any value whose type has x and y of type number is accepted for a Point parameter, no matter how it was declared, and extra members do not disqualify it.

solid answer

~50 s

Structural typing means the compiler asks "does this value have the members `Point` requires, with compatible types?" rather than "was this value declared as a `Point`?". So an object literal stored in a variable, an instance of a class that never writes `implements Point`, or a value of a completely unrelated interface declared in another file all pass, as long as they carry `x: number` and `y: number`. The check runs member by member: every required member of the target must exist in the source with an assignable type. Members the target does not mention are simply ignored, so a richer value is accepted where a narrower type is expected. Nominal languages like Java or C# would reject all of these without an explicit declaration; TypeScript accepts them because the shape is what it models. This is often called duck typing: if it has the members, it counts.

code

typescript · 12 lines
typescript
interface Point { x: number; y: number }

function length(p: Point): number {
  return Math.hypot(p.x, p.y);
}

class Vec { x = 3; y = 4; z = 0 }

const reading = { x: 3, y: 4, sensor: "a1" };

console.log(length(new Vec()));
console.log(length(reading));

go deeper

for a junior

Be ready to say plainly that TypeScript compares the shape of a value, not the name it was declared with, and that an object carrying the required members is accepted with no implements clause anywhere.

for a middle

Explain the member-by-member mechanics: every member the target requires must exist on the source with an assignable type, the comparison recurses into nested objects, and members the target never mentions are ignored.

for a senior

Show where shape-based compatibility misleads a team — two unrelated domain types with identical members swapping silently — and say how you catch that in review and at the boundaries where data enters the system.

for a principal

Own the tradeoff: structural compatibility is why third-party and generated types compose without adapters, but it also means the published contract of your module is a shape, not a name, and any identity you need must be designed in deliberately.

## The rule TypeScript's assignability relation is **structural**: a source type `S` is assignable to a target type `T` when `S` has, member for member, everything `T` requires, with types that are themselves assignable. Nothing about *where* the type was declared, what it is *called*, or whether an `implements` clause was written enters into the decision. Contrast this with a **nominal** system (Java, C#, Kotlin). There, `class Vec` is a `Point` only if the author of `Vec` said so. In TypeScript, saying so is optional — it is a way of asking the compiler to check your class early, not the mechanism that makes the value acceptable. ```ts interface Point { x: number; y: number } function length(p: Point) { return Math.hypot(p.x, p.y); } class Vec { x = 3; y = 4; z = 0 } const reading = { x: 3, y: 4, sensor: "a1" }; length(new Vec()); // ok — no implements clause anywhere length(reading); // ok — sensor is simply not looked at ``` ## How the check actually runs For each member the target declares, the compiler looks the same name up on the source: 1. **Missing required member → error.** `{ x: 1 }` is not a `Point`; `y` is absent. 2. **Present but wrong type → error.** `{ x: 1, y: "4" }` fails on `y`, and the failure is reported as a nested reason ("types of property 'y' are incompatible"). 3. **Present and assignable → keep going.** The member check is itself recursive: nested objects are compared the same way, all the way down. 4. **Extra members on the source → ignored.** The target never says "and nothing else", so a wider value satisfies a narrower type. This is *width subtyping*. Because the relation is defined on shapes and not on names, two interfaces written independently in two files, with no import between them, are interchangeable if their members line up. That is the single most surprising consequence for someone arriving from a nominal language, and it is what the term *duck typing* points at: if it walks and quacks like a `Point`, it is one. ## Two clarifications interviewers listen for **Interfaces are a compile-time device only.** An `interface` emits no JavaScript. There is no runtime object representing `Point`, no tag stamped on values, nothing to check with `instanceof`. Structural typing is not the compiler being lax at runtime — there *is* no runtime type layer at all. A value that satisfied `Point` at the call site is just an ordinary object once compiled. **Structural does not mean "anything goes".** The compiler is strict about what the target requires; it is only silent about what the target does not mention. Excess members are allowed on values, but a *fresh object literal* written directly at the assignment site gets an additional check for unknown properties layered on top — a separate rule, and a common source of the false belief that TypeScript is nominal about object shapes. ## Why the language is designed this way JavaScript code is full of ad-hoc object shapes: options bags, JSON parsed from a response, the result of a `map`, an object spread. Almost none of it was authored against a named type. A nominal system would force an adapter or a cast at every boundary; structural typing lets you *describe* an existing shape after the fact and have existing values satisfy the description immediately. It is also what makes third-party `.d.ts` files usable — your object satisfies a library's interface without your code depending on the library at the value level. The cost is that shape, not name, is your contract. Two domain concepts that happen to be `{ value: number }` — metres and feet, gross and net — are freely interchangeable, and the compiler will never object. That is a real class of production bug, and the reason type systems that need identity add something extra on top of the structure. ## Answering it well Say the rule in one line ("compatibility is decided by members, not by names"), name the direction (a source may have *more* than the target requires, never less), and mention that `implements` is a checking convenience rather than the enabling mechanism. If you get one more sentence, add that the whole comparison disappears at compile time — the emitted JavaScript contains no trace of `Point`.

  • If a class does not need an `implements` clause to satisfy an interface, what does writing one actually buy you?
    It moves the error. Without it, a mismatch is reported at every call site that passes the instance; with it, the class declaration itself fails to compile and names the missing or wrong member. It also documents intent for readers. It never changes whether the value is accepted elsewhere — that is still decided structurally.
  • Does structural typing mean two identically-shaped domain types can be swapped by accident?
    Yes. If `Metres` and `Feet` are both `{ value: number }`, each is assignable to the other and the compiler stays silent, because nothing distinguishes them structurally. Making them distinguishable requires giving one a member the other does not have — the type system will not invent an identity for you.
  • How is a nested member compared — by name too, or by shape?
    By shape, recursively. If the target requires `origin: Point`, the compiler applies the same member-by-member test to whatever the source's `origin` is. The comparison bottoms out at primitives, and a failure deep in the tree is reported as a chain of "types of property … are incompatible" reasons.

A bouncer checking a dress code rather than a guest list: he looks at what you are wearing, not whether your name was written down in advance.

saying these in an interview costs you the question

  • Says a class must declare implements before it can be used
  • Claims interfaces exist at runtime and can be checked
  • Thinks two same-shaped interfaces are incompatible across files
  • Believes extra members always make a value incompatible
  • Calls structural typing "the compiler being loose" rather than a rule

context

open as a page

In TypeScript, `const opts = { url: "/api", retries: 3 }` is passed to `function get(o: { url: string }) {}` as `get(opts)`. Does that compile, and what assignability rule decides it?

level: middleimportance: must knowfreq 64%

basics

~20 s

It compiles. Assignability requires the source to have at least the members the target declares; it never requires the source to have only those members, so the extra retries property is ignored. This allowance is called width subtyping.

open as a page

In TypeScript, which values are assignable to the type `{}`, how does that differ from the type `object`, and why is `{}` a poor way to say "some object"?

level: middleimportance: should knowfreq 40%

basics

~20 s

{} requires no members, so under strictNullChecks every value except null and undefined is assignable to it, primitives included. object excludes primitives but still accepts any non-primitive. Neither lets you read a property, so {} says almost nothing.

open as a page

In TypeScript, a value of type `{ id: string; name?: string }` is assigned to a variable of type `{ id: string; name: string }`. Does it compile, and does the reverse assignment compile?

level: middleimportance: should knowfreq 47%

basics

~20 s

The first fails: a member that is optional on the source may be absent, so it cannot satisfy a required member on the target. The reverse compiles, because a required member always satisfies an optional one.

open as a page

A TypeScript service has `function emit(e: { id: string }) { queue.push(JSON.stringify(e)) }`. In production the queued payloads contain fields nobody declared, including sensitive ones. Explain why the type system permitted this and how you would prevent it.

level: seniorimportance: should knowfreq 36%

basics

~20 s

An object type is a lower bound: it requires at least the declared members and never forbids others. Callers legitimately pass wider objects, and since types are erased, the extra fields are still there when JSON.stringify walks the value at runtime.

open as a page