Расширение и пересечение
Представьте замок посложнее: в нём те же выемки, что в простом, и ещё одна сверху. Ключ от сложного замка откроет и простой — все нужные простому зубцы у него есть. А чтобы сделать такой замок, не нужно чертить его заново: берут чертёж простого и добавляют выемку.
С типами бывает та же задача. Администратор — это пользователь, у которого есть ещё и права. Если переписать в интерфейс Admin все свойства User, придётся следить за двумя копиями: добавили в User свойство — не забудьте про Admin. Вместо копии интерфейс можно расширить, то есть добавить к нему члены через extends; к расширению типа из темы 02 (5 → number) это отношения не имеет. Вот Admin, расширенный из User:
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Давайте разберём пример по строкам:
interface Userтребует два свойства:nameиemail.interface Admin extends Userполучает все членыUserи добавляет свой — массив строкpermissions.- В литерале для
bossдолжны быть все три свойства. Безemailtsc сообщит, что его не хватает, как и для обычногоUser. - Программа печатает
Ann 1.
Как читать extends
Запись interface Admin extends User { … } читается как «Admin — это User и ещё вот что». Отсюда два следствия:
- в фигурных скобках перечисляют только новые члены, а старые приходят из
User; - значение типа
Adminподходит везде, где ждутUser, ведь все свойстваUserу него есть.
Расширять можно сразу несколько интерфейсов — их перечисляют через запятую:
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Пустые скобки {} у Person значат, что своих членов у него нет: всё пришло из HasName и HasAge. Если же Admin объявит name: number, а в User это string, tsc не даст так расширить интерфейс: ошибка TS2430.
Пересечение через &
Тот же Person можно собрать без extends, оператором &:
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 — пересечение типов: значение должно подходить под оба сразу. Это как дверь с двумя замками: войдёт только тот, чей ключ подходит к обоим. Объект без age tsc не пропустит, зато у ann можно читать и name, и age.
Когда свойства у типов разные, extends и & дают одно и то же. Разница в том, где их пишут: extends — в объявлении интерфейса, а & — везде, где пишут тип, например в псевдониме type.
& или |
Знаки & и | часто путают, а различаются они сильно. Чем A | B отличается от A & B — частый вопрос на собеседовании.
Объединение |: одно из двух
Под HasName | HasAge годится любое значение, которое подходит хотя бы под один из типов. Как на парковке со знаком «машины или мотоциклы»: кто бы там ни стоял, можно делать только то, что годится и для машины, и для мотоцикла. Под HasName | HasAge подойдёт и { age: 30 }, где нет name, поэтому прочитать name без проверки tsc не даст.
Пересечение &: оба сразу
Под HasName & HasAge подходит только значение, у которого есть всё, что требуют оба типа. Подходящих значений меньше, зато пользоваться можно всеми свойствами — и name, и age.
Частая ошибка
Хотели объект со свойствами обоих интерфейсов, а написали |:
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'.Вторая строка объясняет причину: в Staff нет свойства name, а p может оказаться именно Staff. То же tsc сообщит и о department, которого нет в User. Заметьте, что TypeScript 7 печатает члены объединения в своём порядке, поэтому в сообщении стоит Staff | User. Нужен был тип User & Staff: тогда у p есть оба свойства.
Главное
interface Admin extends User { … }получает все членыUserи добавляет свои; расширять можно несколько интерфейсов через запятую.A & B— пересечение: значение подходит под оба типа, и можно пользоваться членами обоих.A | B— объединение: значение подходит хотя бы под один тип, и без проверки доступны только общие члены.- Когда свойства у типов разные,
extendsи&дают одно и то же, но&можно писать в любом типе.