Command Palette

Search for a command to run...

Hectal
PHASE 3Beginner ~22 min· topic 3 of 6

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. Dog is a subtype of Animal.
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.

extends.tswhole filets
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.

multi.tswhole filets
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.

intersection.tswhole filets
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.

conflict.tswhole filets
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 never

05Narrowing 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.

narrower.tswhole filets
interface Wide { id: string | number }
interface Narrower extends Wide { id: string }  // fine
Narrowing is alloweddiagram
Rendering diagram…

Try it yourself

  1. 1

    Give Dog a bad id

    Add id: string; to Pet and id: number; to Animal in the first example. Predict the error before you read it: where does it appear, at the Dog declaration or at the object?

    At the Dog declaration: TS2320: Interface 'Dog' cannot simultaneously extend types 'Animal' and 'Pet', with the note Named 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, extends shows right away.

  2. 2

    Fix the intersection

    In the third example change type B = { id: number; size: number }; to { id: string; size: number }. Predict the type of id in the hover.

    Now both pieces agree: id is string and the object on the attempt line only needs id: "1".

  3. 3

    Combine three

    In the second example add type Tagged = { tags: string[] }; and make Draft Person & Tagged & { draft: true }. Predict which missing member the error for bad reports first. It names tags and the type Tagged that demanded it. d is now an error too, because it has no tags either.

Code & diagrams

Extending one or more interfaces

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' ]
Mira
Intersections glue types together

Person 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' }
Conflicts in an intersection become never

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 function

Break 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 }.

terminal
$ Types panel
── what you'll see ──
index.ts(28,11): error TS2430: Interface 'Child' incorrectly extends interface 'Base'.
Types of property 'id' are incompatible.
Type 'number' is not assignable to type 'string'.

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.

terminal
$ Types panel
── what you'll see ──
index.ts(13,25): error TS2322: Type 'number' is not assignable to type 'never'.

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 single interface ... extends (the TypeScript performance guide recommends interfaces that extend each other over intersections).

  • ▸

    Type reduction to never for a conflicting discriminant (a property whose type is a unit literal such as "cat") was added in TypeScript 3.9. It is why Cat & Dog above prints never for the whole value, while a conflicting id: string and id: number only makes that one property never.

  • ▸

    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. 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[] } makes Dog contain everything Animal has, plus tricks. The new interface is a subtype: anything that fits Dog also fits Animal, so a Dog can be passed to a function that wants an Animal. You can extend several at once, separated by commas: interface Dog extends Animal, Pet { ... }.

  2. 2

    An intersection is written A & B and 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 an id and a name. You can intersect aliases, interfaces and inline shapes, and you can chain them: A & B & C. (A union A | B is the opposite idea: either one. That is Topic 4.1.)

  3. 3

    extends and & look similar but check differently when the pieces disagree. With interface Child extends Base, if Child redeclares a property with a type that is not assignable to the one in Base (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 (a string | number can become string), widening or changing it is not.

  4. 4

    With &, there is no refusal at the declaration. { id: string } & { id: number } is accepted, and the property id gets the type string & 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: any id you 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. 5

    There is a special case: when the conflicting property is a literal tag, like kind: "cat" against kind: "dog", TypeScript reduces the whole intersection to never, not just the one property. A Cat & Dog is a value that can never exist. This is deliberate: it is what lets discriminated unions (Topic 4.5) work.

  6. 6

    Rule of thumb: use extends when one thing really is a kind of another (Dog is an Animal), 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 produces never when you didn't expect it, look for the same property name with two different types.

Explain it without notes

01

What is the difference between interface B extends A and type B = A & { ... }?

02

What does the intersection { id: string } & { id: number } evaluate to, and why does the error often appear far from the declaration?

03

How do you choose between extending and intersecting?

04

What does it mean that Dog is a subtype of Animal?

Practice

01

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.

02

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 extends chains 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 TS2430 appears for extends but not for &.

  • I can say "A and B" for & and "A or B" for |.