skip to content

extends vs Intersection (&)

Both `interface B extends A` and `type B = A & { ... }` compose shapes, but they handle conflicts differently: extends reports an incompatible-member error up front, while an intersection silently produces a member typed `never` you only discover at the call site. Interviewers use this to check that you know composition is not symmetric between the two forms.

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

questions

4

In TypeScript, both `interface B extends A { id: number }` and `type B = A & { id: number }` compose A's members with a new one. If A already declares `id: string`, how do the two forms behave differently?

level: middleimportance: must knowfreq 66%

answer

  1. one form is loud, one is silent
  2. override versus combine
  3. assignability checked at the declaration site
  4. conflicting members intersect rather than replace
  5. string & number becomes never

basics

~20 s

extends overrides the inherited member and errors right at the declaration when the new type is not assignable to the old one. An intersection instead combines both types, so id becomes string & number, which reduces to never — a failure you only meet at the use site.

solid answer

~40 s

They differ because `extends` **overrides** while `&` **combines**. With `interface B extends A`, the compiler checks that B is assignable to A: `id: number` against `id: string` fails immediately with "Interface 'B' incorrectly extends interface 'A'", pointing at the declaration. Narrowing is allowed — if A declared `id: string | number`, redeclaring `id: string` compiles and the member is `string`. With `type B = A & { id: number }` there is no check at all; the member's type becomes the intersection `string & number`, which TypeScript reduces to `never`. Nothing is reported where you wrote the type. You discover it later when every attempt to construct or assign a `B` fails, because no value inhabits `never`. That silent-failure asymmetry is the whole point of the question.

code

typescript · 11 lines
typescript
interface A { id: string }

// Intersection: no error here, but the member is uninhabited.
type Merged = A & { id: number };
declare const m: Merged;
const proof: never = m.id;

// Narrowing under extends is legal, because string is assignable to string | number.
interface Wide { id: string | number }
interface Narrow extends Wide { id: string }
const n: Narrow = { id: "ok" };

go deeper

for a junior

Know that both forms add members to an existing shape, and that redeclaring a member with a conflicting type is a mistake — the interface form tells you so immediately.

for a middle

Be ready to explain that extends checks assignability and overrides the member, while & computes the intersection of both member types and reduces incompatible ones to never with no diagnostic.

for a senior

Show you can diagnose the symptom in real code: a baffling "not assignable" error at a call site whose real cause is a never member produced by an intersection written elsewhere.

for a principal

Own the guidance for a shared codebase: interfaces for composing named public shapes because failures land at the declaration and member lists are cached, intersections reserved for unions, conditionals and generic operands.

## Two ways to compose a shape TypeScript gives you two syntactic ways to say "this shape is that shape plus more": ```typescript interface A { id: string } interface B1 extends A { createdAt: Date } type B2 = A & { createdAt: Date }; ``` When the added members do not collide with the inherited ones, the two results are for practical purposes interchangeable — both types have `id: string` and `createdAt: Date`, and each is assignable to the other. The interesting behaviour appears only when a name collides. ## `extends` performs a check; the member is overridden A heritage clause is a *constraint*. The compiler verifies that the derived interface is assignable to each parent, and the derived declaration of a name replaces the inherited one. So the rule is: **a redeclared member must be assignable to the member it hides.** ```typescript interface Wide { id: string | number } interface Narrow extends Wide { id: string } // OK — string is assignable to string | number ``` `Narrow['id']` is exactly `string`. Reverse the relationship and the check fails: ```typescript interface A { id: string } interface B extends A { id: number } // Error: Interface 'B' incorrectly extends interface 'A'. // Types of property 'id' are incompatible. Type 'number' is not assignable to type 'string'. ``` The important property is *where* the error lands: on the declaration of `B`, in the file where the mistake was made, before any value exists. ## `&` performs no check; the member types are intersected An intersection is not inheritance and there is no notion of "the later one wins". `A & { id: number }` has one member `id` whose type is the intersection of the two contributions, `string & number`. Since no value is simultaneously a string and a number, TypeScript reduces that intersection to `never`. ```typescript interface A { id: string } type B = A & { id: number }; declare const b: B; const check: never = b.id; // compiles: b.id really is never ``` Writing the type is legal. Nothing is reported. The type is simply uninhabited in that member, and every later attempt to produce a `B` fails: ```typescript const broken: B = { id: 1 }; // Error here, far from the declaration ``` When the colliding members are *literal* (unit) types acting as discriminants, the reduction can go further and collapse the whole object type to `never`, not just the one member — so `{ kind: "a" } & { kind: "b" }` is an unusable type overall. Either way you learn about it at the call site, which may be in a different package. ## Why one errors and the other does not The asymmetry follows from what each construct *means*. `extends` states a subtype relationship, and a subtype relationship is checkable at the point of declaration, so the compiler checks it. `&` is a type-level operator: it computes a type the way `+` computes a number. An operator has no obligation to be satisfiable — `string & number` is as well-formed as it is useless, exactly like `never` itself. Reporting an error there would break legitimate generic code such as `T & U`, where the operands are unknown at the time the alias is written. ## Multiple parents With several parents the check tightens. `interface C extends A, B` requires any property inherited from both to have **identical** types, not merely compatible ones, unless `C` redeclares it: ```typescript interface A { id: string } interface B { id: number } interface C extends A, B {} // Error: Interface 'C' cannot simultaneously extend types 'A' and 'B'. // Named property 'id' of types 'A' and 'B' are not identical. ``` The intersection `A & B` again stays quiet and hands you `id: never`. ## Practical consequences - Prefer `extends` when composing named object shapes. It is the earlier, louder failure, its hover output is a single named type rather than a chain of `&`, and the TypeScript team's own performance guidance recommends interfaces over large intersections because an interface's member list is computed once and cached, whereas an intersection's members are recomputed during comparisons. - Reach for `&` when the left operand is something an interface cannot extend — a union, a conditional type, or a bare type parameter in generic code. - When you must intersect, treat `never` members as the diagnostic. If a value "should obviously be assignable" and the error is baffling, hover the member: a `never` there means two constituents disagreed and nobody told you. - None of this survives compilation. Both forms are erased entirely; the difference is purely in what the checker says and when it says it.

  • What happens with `interface C extends A, B` when A and B each declare `id` with a different type?
    It is an error: "Interface 'C' cannot simultaneously extend types 'A' and 'B'" because the named property is not identical in both. Note the bar is *identical*, not merely compatible — inheriting `string` from one parent and `string | number` from the other is still rejected unless `C` redeclares `id` itself with a type assignable to both.
  • If narrowing a member under `extends` is legal, do `extends` and `&` ever produce the same member type?
    Yes, whenever the two types are compatible. With `id: string | number` inherited and `id: string` redeclared, `extends` gives `string` and the intersection gives `(string | number) & string`, which reduces to `string`. The forms diverge only on incompatible members, where one errors and the other silently yields `never`.
  • Why is `string & number` reduced to `never` rather than reported as an error?
    `&` is a type operator, not an assertion. It computes a type the way arithmetic computes a value, and an uninhabited result is well-formed. Erroring would break generic code like `T & U`, where the operands are unknown when the alias is written and may legitimately be disjoint for some instantiations.

saying these in an interview costs you the question

  • Says the later declaration wins, so id becomes number
  • Claims both forms report the same compiler error
  • Thinks never means the property was dropped from the type
  • Believes any redeclaration under extends is an error, even a narrower one
  • Expects the intersection error to appear where the type is declared

context

open as a page

In TypeScript, which kinds of `type` alias can an `interface` extend in its `extends` clause, and which ones does the compiler reject?

level: juniorimportance: should knowfreq 44%

basics

~20 s

An interface can extend an alias that resolves to an object type, or to an intersection of object types, as long as its members are statically known. Aliases for unions, primitives or unresolved type parameters are rejected.

open as a page

In TypeScript, does swapping the operands of an intersection type — writing `B & A` instead of `A & B` — ever change how the resulting type behaves?

level: seniorimportance: should knowfreq 30%

basics

~20 s

For property members, no: each member's type is the intersection of both contributions, and that is commutative, so the two orderings are mutually assignable. Order does matter for call and construct signatures, which are concatenated in operand order and resolved first-match-wins.

open as a page

In TypeScript an interface may extend a class, as in `interface Renderable extends Widget { label: string }`. What does the interface inherit from the class, and what restricts which types can implement it?

level: middleimportance: nice to knowfreq 26%

basics

~20 s

The interface inherits the class's instance-side members — property and method types, without any implementations, and without the constructor or statics. It also inherits private and protected members, and because those are nominal, only the class itself or a subclass can implement the interface.

open as a page