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?
answer
- assignability is a subset test
- only the target's members are examined
- more is fine, less is not
- width subtyping, no exact object types
basics
~20 sIt 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.
solid answer
~50 sYes, it compiles. `opts` has type `{ url: string; retries: number }`, and assignability asks only whether every member of the target — here just `url: string` — is present on the source with a compatible type. Nothing in the target says "and no other members", so `retries` is invisible to the check. That is width subtyping, and it is what lets a rich value flow into a function that needs a narrow slice of it. Note the direction: a source may have more than the target requires, never less; drop `url` and the same assignment fails. One thing to flag: had the same literal been written inline as `get({ url: "/api", retries: 3 })`, an additional check for unknown properties on fresh object literals applies — a separate rule layered on top of assignability, which is why the variable and the inline literal behave differently.
code
typescript · 9 linesinterface Opts { url: string }
function get(o: Opts): string { return o.url; }
const opts = { url: "/api", retries: 3 };
console.log(get(opts));
const thin = { url: "/api" };
const wide: { url: string; retries: number } = { ...thin, retries: 0 };
console.log(wide.retries);go deeper
Recall the direction: a value may carry more members than the type asks for, but never fewer. Be able to say the call compiles and point at which member the target actually required.
Explain that assignability inspects only the target's members, name it as width subtyping, and separate it cleanly from the extra check that fires on a fresh object literal written inline.
Draw the consequence for real systems: an object parameter is a lower bound, so anything you serialise or enumerate from it may contain more than the type shows, and precise payloads must be constructed rather than forwarded.
Frame the design choice — TypeScript has no exact object type, so "no unexpected members" is a property you enforce at boundaries with validation and explicit construction, not something the type layer will ever assert for you.
## The rule being applied Assignability between object types is a **subset test on the target's requirements**, not an equality test on member sets. To decide `S` is assignable to `T`, the compiler walks the members `T` declares and, for each one, requires a compatible member on `S`. It never walks `S`'s members looking for surprises. So: ```ts interface Opts { url: string } function get(o: Opts) { return o.url; } const opts = { url: "/api", retries: 3 }; get(opts); // ok — retries is never examined ``` This one-directional allowance is **width subtyping** (the source is "wider" — it has more columns). Its counterpart, *depth* subtyping, is the recursive part: a member's own type may itself be a subtype, so `{ url: string; meta: { id: string; ts: number } }` satisfies `{ url: string; meta: { id: string } }`. ## Why the direction matters Width subtyping is sound for reading. A function that only reads `o.url` cannot be harmed by the presence of `retries`; every operation the type permits still works. Reverse it and the guarantee collapses: ```ts const thin = { url: "/api" }; const wide: { url: string; retries: number } = thin; // error: retries is missing ``` Here the target promises a `retries` a reader may access, and the source cannot deliver it. "More is fine, less is not" is the sentence to carry into the interview. ## The exception that confuses everyone The assignability rule above is complete, and yet this fails: ```ts get({ url: "/api", retries: 3 }); // ^ object literal may only specify known properties ``` The object is assignable by the rule; a *second*, separate check rejects it. That check applies only to **fresh object literals** — a literal written directly at the assignment or argument position — on the theory that an unknown property there is almost certainly a typo or a misremembered option name, not a deliberate wider value. Assign the same literal to a variable first and its freshness is gone, so only the ordinary assignability rule remains and the call compiles. Getting the two straight is most of what this question tests. Assignability is the general relation; the literal check is a targeted usability heuristic on top of it. It is emphatically not the case that TypeScript "sometimes" forbids extra properties on values. ## What this does and does not guarantee Because extras are permitted, an object-typed parameter is a **lower bound**, never an upper bound. `o: { url: string }` means "at least a string url"; the object arriving at runtime may carry anything else, and it will. Code that enumerates the parameter — `Object.keys(o)`, `JSON.stringify(o)`, spreading it into a request body — sees the extras, because the annotation was erased and never described the runtime object exhaustively. TypeScript has no *exact* object type that would say "these members and no others". A practical consequence: when a value must have a precise shape on the wire, build that shape explicitly rather than forwarding an object that merely satisfies the type. ```ts function toBody(o: { url: string }) { return { url: o.url }; // construct, don't forward } ``` ## Related mechanics worth one sentence each - The check is structural throughout: `opts` never declared any relationship to `Opts`, and none is needed. - Optional members in the target relax the requirement further — the source may omit them entirely. - Assignability is checked at every position: arguments, `return`, variable initialisers, and the members of a larger object being compared. ## The short answer It compiles because assignability only demands that the target's members be present and compatible; extras are outside the question. Reverse the direction and it fails. And the inline-literal version behaves differently not because assignability changed, but because a separate freshness check fires on literals.
- Does the same allowance hold when the extra member sits on a nested object rather than the top level?Yes. The comparison recurses: for a target member `meta: { id: string }`, the source's `meta` is checked by the same rule, so `meta: { id: string; ts: number }` is fine. Width and depth compatibility apply at every nesting level, and a mismatch deep in the tree is reported as a chain of incompatible-property reasons.
- If extras are permitted, why does a function typed to take `{ url: string }` still not let me read `o.retries` inside it?Because the parameter's *declared* type is what the body may use, and it declares only `url`. The extra member may exist on the value at runtime, but the compiler models the parameter as the narrow type; reading an undeclared member is an error. Widening the parameter type, or accepting a generic, is the way to see it.
- Which direction of this rule would break if object members were writable through the target type?Depth compatibility. Reading `{ id: string; ts: number }` as `{ id: string }` is safe, but if the target let you *replace* a nested member with a value that satisfies only the narrower type, the wider guarantee is lost. TypeScript accepts this unsoundness for ergonomics; `readonly` members do not prevent it either.
saying these in an interview costs you the question
- Says TypeScript forbids extra properties on assignment, full stop
- Explains the literal error as the general assignability rule
- Thinks a missing member is allowed if it is only sometimes used
- Claims the parameter type guarantees no other members at runtime
- Believes assigning the literal to a variable is a workaround for a bug