skip to content

In a TypeScript conditional type, `extends` is often described as "assignable to" rather than "inherits from". What does `type R = { id: number; name: string } extends { id: number } ? 'yes' : 'no'` resolve to, and why?

level: middleimportance: must knowfreq 58%

answer

  1. not the same extends as on classes
  2. structural, never declared
  3. more members means more assignable
  4. direction matters: specific on the left
  5. any in the check yields both branches

basics

~20 s

The result is 'yes'. In a conditional type, extends asks whether the left type is assignable to the right one, and an object type with an extra property is assignable to one that requires fewer. No declared inheritance is involved.

solid answer

~40 s

It resolves to `'yes'`. The `extends` in a conditional type is not the class keyword — it is an assignability test, and TypeScript's assignability is structural. `{ id: number; name: string }` has everything `{ id: number }` requires, plus more, so it is usable wherever the smaller type is expected and the true branch wins. The direction is what trips people up: the *more specific* type goes on the left. Reverse the two and you get `'no'`, because `{ id: number }` is missing the required `name`. The same rule explains why `'abc' extends string` passes and why `Date extends { getTime(): number }` passes even though `Date` was never declared to implement anything.

code

typescript · 7 lines
typescript
type Extends<A, B> = A extends B ? 'yes' : 'no';

type R1 = Extends<{ id: number; name: string }, { id: number }>; // 'yes'
type R2 = Extends<{ id: number }, { id: number; name: string }>; // 'no'
type R3 = Extends<'abc', string>;                                // 'yes'
type R4 = Extends<Date, { getTime(): number }>;                  // 'yes'
type R5 = Extends<{ id: number }, { id: number; name?: string }>; // 'yes'

go deeper

for a junior

Be able to say that extends here means "is assignable to" and not "inherits from", and answer correctly for the simple case where the left type has an extra property.

for a middle

Explain the direction rule — the more specific type on the left — and demonstrate it with a pair of object types plus a literal-versus-primitive example. Say plainly that the comparison is structural.

for a senior

Show that you have been burned by the edge cases: any yielding both branches, optional members loosening the target, and a broad object check quietly matching richer shapes than intended.

for a principal

Be ready to argue where structural assignability is the right foundation for a shared type contract and where a team needs branded or nominal-style types instead to stop unrelated shapes from satisfying a check.

## The question the checker actually asks When the compiler meets `A extends B ? X : Y`, it runs exactly one test: **is `A` assignable to `B`?** That is, could you pass a value of type `A` into a parameter declared as `B` without an error? If yes, the conditional becomes `X`; if no, it becomes `Y`. The keyword is unfortunate. `extends` also appears on classes (`class Dog extends Animal`) and as a constraint on a type parameter, and those are different jobs. In the conditional position it is neither a declaration nor a restriction — it is a yes/no question about compatibility. ## Structural, not declarative TypeScript's assignability is structural: what matters is the *shape* of the type, not what it was declared to inherit or implement. So the test passes for types that have no declared relationship whatsoever: ```ts type Extends<A, B> = A extends B ? 'yes' : 'no'; type R1 = Extends<Date, { getTime(): number }>; // 'yes' ``` `Date` never says it implements that object type; it simply has a `getTime` method returning a number, and that is enough. Any question of the form "does X extend Y here" is answered by comparing members, not by looking up a declaration. ## Direction: the more specific type goes on the left This is the single most common mistake. Adding members makes a type *more* specific and *more* assignable, not less: ```ts type R2 = Extends<{ id: number; name: string }, { id: number }>; // 'yes' type R3 = Extends<{ id: number }, { id: number; name: string }>; // 'no' ``` In `R2` the left type satisfies every requirement of the right type — it has an `id` of type `number` — and the extra `name` is simply not asked about. In `R3` the left type is missing a required member, so the test fails. The same asymmetry shows up with literals and primitives: ```ts type R4 = Extends<'abc', string>; // 'yes' — a literal is assignable to its primitive type R5 = Extends<string, 'abc'>; // 'no' — string could be any string ``` A useful mental sentence: *the left side must be at least as specific as the right side.* ## Optional members loosen the requirement An optional property is not a requirement, so it does not have to be present: ```ts type R6 = Extends<{ id: number }, { id: number; name?: string }>; // 'yes' ``` The right-hand type only demands `id`; `name` may be absent. This is worth internalising, because it means "has fewer members" and "is not assignable" are not the same statement — it depends which members are required. One related rule does *not* apply here: the excess-property check that rejects `{ id: 1, oops: 2 }` when assigning a fresh object literal to a typed variable is a check on literal expressions in value positions. Type-level assignability, which is what a conditional runs, does not perform it — extra members are simply ignored. ## The `any` special case One result surprises almost everyone: ```ts type R7 = Extends<any, string>; // 'yes' | 'no' ``` When the checked type is `any`, the compiler cannot honestly answer the question either way, so the conditional resolves to the **union of both branches**. This is a documented special rule and a real hazard: a stray `any` leaking into a type-level helper does not pick the convenient branch, it widens the result to everything the helper could ever produce, and the imprecision spreads to whatever consumes it. ## Why this matters beyond trivia Every conditional type you write — and every built-in one — rests on this test. When a conditional "gives the wrong answer", the fix is nearly always to re-read the check as an assignability question and confirm which side is more specific, rather than to reach for an assertion. It also explains a subtlety of writing checks against object types: `T extends { kind: string }` passes for *any* object with a string-typed `kind`, including `{ kind: 'circle'; radius: number }`. A check written to catch one shape will happily catch every richer shape too, which matters as soon as several checks are combined. ## What to say in an interview State the answer (`'yes'`), then the rule (`extends` in a conditional means assignable-to), then the direction (more specific on the left), then one example with no declared relationship at all to prove it is structural. Mentioning the `any` case shows you have actually been surprised by this in practice.

  • What does `type R = any extends string ? 'yes' : 'no'` give you?
    `'yes' | 'no'`. When the checked type is `any`, the compiler cannot commit to a branch and resolves the conditional to the union of both. It is a genuine special rule, and it means a leaked `any` silently widens every downstream type-level result rather than picking one side.
  • Does the excess-property check apply inside a conditional type's extends test?
    No. Excess-property checking fires when a fresh object literal is assigned in a value position; it is a deliberate extra strictness on literals. The type-level assignability a conditional runs simply ignores members the target does not mention, which is why the wider object type passes.
  • How would you check that two object types are exactly the same rather than merely assignable?
    A single `extends` cannot do it, because assignability is one-directional. The usual approach is a mutual test — check `A extends B` and `B extends A` — which rules out the case where one side has extra required members. It still treats structurally identical types as equal, since TypeScript is structural.

saying these in an interview costs you the question

  • Says the smaller type extends the bigger one
  • Thinks a declared class or interface relationship is required
  • Assumes extra properties break compatibility
  • Expects `any` on the left to pick one branch
  • Confuses it with the constraint form on a type parameter

context