Topic 3.3
Extending and Combining Shapes
In one line
Build big shapes out of small ones: an interface can extend one or more others, and the & operator glues any types into an intersection. When the pieces disagree about a property, the result can quietly become never.
Think of it like this
Building with LEGO sets. A "Pet" set has an owner tag. An "Animal" set has a name and a sound. A Dog is both sets stuck together, plus a few extra bricks for tricks. You don't remake the bricks, you join the sets. **extends and & are the ways to stick shape-sets together.**
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- extends
- Keyword that makes a new interface include all members of one or more existing ones.
- Subtype
- A type that can be used anywhere its parent type is expected.
Dogis a subtype ofAnimal. - Intersection
- A type written
A & B: a value must satisfy both. For objects it has the members of both. - Union
- A type written
A | B: a value satisfies at least one of them. - never
- The type with no possible values. Showing up where you didn't expect it usually means a contradiction.
- Conflict
- Two pieces that declare the same property with incompatible types.
- Mix in
- Add a few extra members to an existing type without making a whole new named type.
Step by step
01Extend one interface
extends copies every member of the parent into the child. You write only what is new.
A child can be used anywhere a parent is expected, because it has everything the parent promised.
interface Animal {
name: string;
sound(): string;
}
interface Dog extends Animal {
tricks: string[];
}02Extend several at once
List parents separated by commas. The child gets members from all of them.
If two parents declare the same property with incompatible types, the child declaration is an error, so you find out immediately.
interface Pet {
owner: string;
}
interface Dog extends Animal, Pet {
tricks: string[];
}03Combine with &
& works on any types and needs no new name. Person & { draft: true } is "a person that is also marked as a draft".
Object literals are checked against every part. Leave out a member from any part and you get a missing-property error that names it.
type HasId = { id: number };
type HasName = { name: string };
type Person = HasId & HasName;
type Draft = Person & { draft: true };
const bad: Draft = { id: 3, name: "Zed" };
// error TS2322: ... Property 'draft' is missing in type '{ id: number; name: string; }' but required in type '{ draft: true; }'.04When the pieces disagree
With extends, a conflict is an error at the declaration. With &, the conflicting property silently becomes never.
Here the same name id is a string in one piece and a number in the other. The result has an id that no value can satisfy.
interface Base { id: string }
interface Child extends Base { id: number }
// error TS2430: Interface 'Child' incorrectly extends interface 'Base'.
type Both = { id: string } & { id: number };
// no error here, but Both["id"] is never05Narrowing is allowed
A child may make a property more specific, because a narrower type still satisfies the parent's promise. string is fine where string | number was promised.
This is how real code specialises a general interface.
interface Wide { id: string | number }
interface Narrower extends Wide { id: string } // fineTry it yourself
- 1
Give Dog a bad id
Add
id: string;toPetandid: number;toAnimalin the first example. Predict the error before you read it: where does it appear, at theDogdeclaration or at the object?At the
Dogdeclaration:TS2320: Interface 'Dog' cannot simultaneously extend types 'Animal' and 'Pet', with the noteNamed property 'id' of types 'Animal' and 'Pet' are not identical.More errors follow on the objects, but the first one is at the declaration. A mistake that&hides,extendsshows right away. - 2
Fix the intersection
In the third example change
type B = { id: number; size: number };to{ id: string; size: number }. Predict the type ofidin the hover.Now both pieces agree:
idisstringand the object on theattemptline only needsid: "1". - 3
Combine three
In the second example add
type Tagged = { tags: string[] };and makeDraftPerson & Tagged & { draft: true }. Predict which missing member the error forbadreports first. It namestagsand the typeTaggedthat demanded it.dis now an error too, because it has notagseither.
Code & diagrams
A Dog is passed to describe(pet: Pet) without any cast: it has everything Pet promised. The last line forgets tricks, and the error names the missing member and the type that demanded it.
Console
(nothing was printed)Expected console output
Rex Mira Woof [ 'sit' ]
MiraPerson is the union of two small shapes. Draft adds a third, written inline. The error lists what is missing from the whole combined type.
Console
(nothing was printed)Expected console output
{ id: 1, name: 'Asha' }The declaration of Both is accepted. The trouble appears when you read id (it is never) or try to build a Both: no number and no string is a never. label, which only A declares, is fine.
Console
(nothing was printed)Expected console output
compile-time only function functionBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Make a child that contradicts its parent
At the end of the first example add interface Base { id: string } and, on the next line, interface Child extends Base { id: number }.
Break #2
Intersect two different id types
In the third example, build a Both and give it a number id. Line 13, const attempt: Both = { id: 1, label: "x", size: 2 };, already does exactly that.
Myth vs fact
Myth
A & B means "A or B".
Fact
It means both at the same time. The "or" is a union, written A | B. The symbols are easy to swap, so say them aloud: & is "and".
Myth
interface extends and & are two spellings of the same thing.
Fact
They build similar shapes, but extends checks for conflicts at the declaration while & accepts anything and may produce never properties later.
Myth
Intersecting two types always gives something useful.
Fact
Two types with no common values give never. For objects with a clashing literal tag, the whole intersection becomes never.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Intersections of object types are not merged into one object. The checker keeps the list of parts and resolves each property by intersecting its type across the parts. That is why conflicts show up as property-level
never, and why big chains of&are slower than a singleinterface ... extends(the TypeScript performance guide recommends interfaces that extend each other over intersections). - ▸
Type reduction to
neverfor a conflicting discriminant (a property whose type is a unit literal such as"cat") was added in TypeScript 3.9. It is whyCat & Dogabove printsneverfor the whole value, while a conflictingid: stringandid: numberonly makes that one propertynever. - ▸
Intersecting a function type with an object type is a common way to model "a function with properties" (
(() => void) & { displayName: string }), and intersecting two function types gives an overloaded function (Topic 2.5). Both are legal; just remember the result is a single value that must satisfy every part.
Remember this
- 1
The problem this solves: shapes overlap. Every dog has everything an animal has, and copying those members into each new interface means fixing them in many places later.
interface Dog extends Animal { tricks: string[] }makesDogcontain everythingAnimalhas, plustricks. The new interface is a subtype: anything that fitsDogalso fitsAnimal, so aDogcan be passed to a function that wants anAnimal. You can extend several at once, separated by commas:interface Dog extends Animal, Pet { ... }. - 2
An intersection is written
A & Band means "a value that satisfies both A and B at once". For object types it means the properties of both:{ id: number } & { name: string }is an object with anidand aname. You can intersect aliases, interfaces and inline shapes, and you can chain them:A & B & C. (A unionA | Bis the opposite idea: either one. That is Topic 4.1.) - 3
extendsand&look similar but check differently when the pieces disagree. Withinterface Child extends Base, ifChildredeclares a property with a type that is not assignable to the one inBase(a value of the new type could not be used where the old type is expected), the checker refuses right at the declaration:TS2430: Interface 'Child' incorrectly extends interface 'Base'. Narrowing a property is allowed (astring | numbercan becomestring), widening or changing it is not. - 4
With
&, there is no refusal at the declaration.{ id: string } & { id: number }is accepted, and the propertyidgets the typestring & number. No value is both a string and a number, so TypeScript simplifies that to **never**, the type with no values. The mistake only surfaces later, when you try to create such an object: anyidyou write is an error (Type 'number' is not assignable to type 'never'). Read the third example slowly: this is the single most confusing intersection behaviour. - 5
There is a special case: when the conflicting property is a literal tag, like
kind: "cat"againstkind: "dog", TypeScript reduces the whole intersection tonever, not just the one property. ACat & Dogis a value that can never exist. This is deliberate: it is what lets discriminated unions (Topic 4.5) work. - 6
Rule of thumb: use
extendswhen one thing really is a kind of another (Dogis anAnimal), because you get early errors for conflicts. Use&for mixing in extra bits on the fly (Person & { draft: true }) or when you combine things that are not interfaces, such as a union. If an intersection producesneverwhen you didn't expect it, look for the same property name with two different types.
Explain it without notes
What is the difference between interface B extends A and type B = A & { ... }?
What does the intersection { id: string } & { id: number } evaluate to, and why does the error often appear far from the declaration?
How do you choose between extending and intersecting?
What does it mean that Dog is a subtype of Animal?
Practice
Write interfaces Shape { name: string }, Coloured { colour: string } and Circle that extends both and adds radius: number. Create a circle and pass it to a function label(s: Shape): string.
Declare type Order = { id: number; total: number } & { id: string }. What is the type of Order["id"]? Fix it so an order can be created with id: 1.
Trade-offs
- ↔
Deep
extendschains make code easy to find but couple many types to a base. Prefer small, focused interfaces combined where needed over one giant base. - ↔
Intersections are flexible but hide contradictions until use. If you intersect types you don't control, check the result with a
// ^?hover.
Done when you can
I can extend one or more interfaces and say what the child contains.
I can combine types with
&and read the resulting error messages.I can predict when an intersection property becomes
never.I can explain why
TS2430appears forextendsbut not for&.I can say "A and B" for
&and "A or B" for|.