Skip to content
Theory · Interfaces and structural typing · lesson 3 of 12

Extending and intersecting

Picture a more complex lock: it has the same notches as a simple one, plus one more on top. A key to the complex lock also opens the simple one — it has every tooth the simple lock needs. And to make such a lock, nobody draws it from scratch: they take the simple lock's drawing and add a notch.

Types face the same task. An admin is a user who also has permissions. If you copy every property of User into an Admin interface, you have two copies to keep in sync: add a property to User, and do not forget Admin. Instead of copying, you can extend the interface:

TypeScript
interface User {
    name: string;
    email: string;
}

interface Admin extends User {
    permissions: string[];
}

const boss: Admin = {
    name: "Ann",
    email: "ann@example.com",
    permissions: ["delete"],
};
console.log(boss.name, boss.permissions.length); // Ann 1

Let's go through the example line by line:

  1. interface User requires two properties: name and email.
  2. interface Admin extends User gets every member of User and adds its own — the array of strings permissions.
  3. The literal for boss must have all three properties. Without email, tsc reports that it is missing, the same as for a plain User.
  4. The program prints Ann 1.

How to read extends

interface Admin extends User { … } reads as "Admin is a User, plus this". Two things follow from that:

  • the curly braces list only the new members, and the old ones come from User;
  • a value of type Admin fits wherever a User is expected, since it has every property of User.

You can extend several interfaces at once, listed with commas:

TypeScript
interface HasName {
    name: string;
}

interface HasAge {
    age: number;
}

interface Person extends HasName, HasAge {}

const ann: Person = { name: "Ann", age: 30 };
console.log(ann.name, ann.age); // Ann 30

The empty braces {} on Person mean it has no members of its own: everything came from HasName and HasAge. If Admin declared name: number while User has string, tsc would not let it extend the interface: error TS2430.

An intersection with &

The same Person can be built without extends, with the & operator:

TypeScript
interface HasName {
    name: string;
}

interface HasAge {
    age: number;
}

type Person = HasName & HasAge;

const ann: Person = { name: "Ann", age: 30 };
console.log(ann.name, ann.age); // Ann 30

HasName & HasAge is an intersection of types: a value must fit both at once. It is like a door with two locks: only someone whose key fits both gets in. tsc rejects an object without age, but on ann you can read both name and age.

When the types have different properties, extends and & give the same result. The difference is where you write them: extends goes in an interface declaration, while & goes anywhere a type is written, for example in a type alias.

& or |

& and | often get mixed up, yet they differ a lot. How A | B differs from A & B is a common interview question.

The union |: one of the two

Any value that fits at least one of the types is fine for HasName | HasAge. It is like a parking lot with the sign "cars or motorcycles": whoever is parked there, you may only do what suits both a car and a motorcycle. { age: 30 }, which has no name, fits HasName | HasAge too, so tsc will not let you read name without a check.

The intersection &: both at once

Only a value that has everything both types require fits HasName & HasAge. Fewer values fit, but you may use every property — both name and age.

Common mistake

The goal was an object with the properties of both interfaces, but | was written:

TypeScript
interface User {
    name: string;
}

interface Staff {
    department: string;
}

function badge(p: User | Staff): string {
    return `${p.name}, ${p.department}`; // ✗
}
TEXT
error TS2339: Property 'name' does not exist on type 'Staff | User'.
  Property 'name' does not exist on type 'Staff'.

The second line gives the reason: Staff has no property name, and p may turn out to be a Staff. tsc reports the same about department, which User does not have. Note that TypeScript 7 prints the members of a union in its own order, which is why the message says Staff | User. The type needed was User & Staff: then p has both properties.

Key points

  • interface Admin extends User { … } gets every member of User and adds its own; several interfaces can be extended, separated by commas.
  • A & B is an intersection: the value fits both types, and the members of both are usable.
  • A | B is a union: the value fits at least one type, and only the shared members are usable without a check.
  • When the types have different properties, extends and & give the same result, but & can be written in any type.