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:
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 1Let's go through the example line by line:
interface Userrequires two properties:nameandemail.interface Admin extends Usergets every member ofUserand adds its own — the array of stringspermissions.- The literal for
bossmust have all three properties. Withoutemail, tsc reports that it is missing, the same as for a plainUser. - 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
Adminfits wherever aUseris expected, since it has every property ofUser.
You can extend several interfaces at once, listed with commas:
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 30The 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:
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 30HasName & 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:
interface User {
name: string;
}
interface Staff {
department: string;
}
function badge(p: User | Staff): string {
return `${p.name}, ${p.department}`; // ✗
}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 ofUserand adds its own; several interfaces can be extended, separated by commas.A & Bis an intersection: the value fits both types, and the members of both are usable.A | Bis 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,
extendsand&give the same result, but&can be written in any type.