Перейти к содержимому
Теория · Интерфейсы и структурная типизация · урок 3 из 12

Расширение и пересечение

Представьте замок посложнее: в нём те же выемки, что в простом, и ещё одна сверху. Ключ от сложного замка откроет и простой — все нужные простому зубцы у него есть. А чтобы сделать такой замок, не нужно чертить его заново: берут чертёж простого и добавляют выемку.

С типами бывает та же задача. Администратор — это пользователь, у которого есть ещё и права. Если переписать в интерфейс Admin все свойства User, придётся следить за двумя копиями: добавили в User свойство — не забудьте про Admin. Вместо копии интерфейс можно расширить, то есть добавить к нему члены через extends; к расширению типа из темы 02 (5 → number) это отношения не имеет. Вот Admin, расширенный из User:

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

Давайте разберём пример по строкам:

  1. interface User требует два свойства: name и email.
  2. interface Admin extends User получает все члены User и добавляет свой — массив строк permissions.
  3. В литерале для boss должны быть все три свойства. Без email tsc сообщит, что его не хватает, как и для обычного User.
  4. Программа печатает Ann 1.

Как читать extends

Запись interface Admin extends User { … } читается как «Admin — это User и ещё вот что». Отсюда два следствия:

  • в фигурных скобках перечисляют только новые члены, а старые приходят из User;
  • значение типа Admin подходит везде, где ждут User, ведь все свойства User у него есть.

Расширять можно сразу несколько интерфейсов — их перечисляют через запятую:

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

Пустые скобки {} у Person значат, что своих членов у него нет: всё пришло из HasName и HasAge. Если же Admin объявит name: number, а в User это string, tsc не даст так расширить интерфейс: ошибка TS2430.

Пересечение через &

Тот же Person можно собрать без extends, оператором &:

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 — пересечение типов: значение должно подходить под оба сразу. Это как дверь с двумя замками: войдёт только тот, чей ключ подходит к обоим. Объект без 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.

Частая ошибка

Хотели объект со свойствами обоих интерфейсов, а написали |:

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'.

Вторая строка объясняет причину: в Staff нет свойства name, а p может оказаться именно Staff. То же tsc сообщит и о department, которого нет в User. Заметьте, что TypeScript 7 печатает члены объединения в своём порядке, поэтому в сообщении стоит Staff | User. Нужен был тип User & Staff: тогда у p есть оба свойства.

Главное

  • interface Admin extends User { … } получает все члены User и добавляет свои; расширять можно несколько интерфейсов через запятую.
  • A & B — пересечение: значение подходит под оба типа, и можно пользоваться членами обоих.
  • A | B — объединение: значение подходит хотя бы под один тип, и без проверки доступны только общие члены.
  • Когда свойства у типов разные, extends и & дают одно и то же, но & можно писать в любом типе.