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

Структурная типизация

Вернёмся к замку. Мастер сделал два ключа с одинаковыми зубцами: на головке одного выбито «Point», на головке другого — «Vec». Замок откроют оба. Надпись на головке он не читает, его интересуют только зубцы.

TypeScript рассуждает так же. Пусть в программе есть точка на карте Point и сдвиг Vec. По смыслу это разные вещи, но устроены они одинаково: у обоих числа x и y. Передадим сдвиг туда, где ждут точку:

TypeScript
interface Point {
    x: number;
    y: number;
}
interface Vec {
    x: number;
    y: number;
}

function show(p: Point): string {
    return `(${p.x}, ${p.y})`;
}

const shift: Vec = { x: 3, y: 4 };
console.log(show(shift)); // (3, 4)

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

  1. Point и Vec — два разных интерфейса с одинаковыми членами.
  2. Функция show ждёт Point.
  3. Константа shift объявлена как Vec, и нигде не сказано, что она Point.
  4. Вызов show(shift) всё равно проходит: у shift есть x и y, оба числа, — ровно то, что требует Point. Программа печатает (3, 4).

Форма, а не имя

Такой способ сравнивать типы называется структурной типизацией: значение подходит под тип, если у него есть все нужные свойства нужных типов. Как называется тип значения и где его объявили, неважно.

Во многих языках, например в Java и C#, правило другое — номинальная типизация. Там Vec подошёл бы туда, где ждут Point, только если бы в объявлении Vec было прямо написано, что это Point, — например, через extends. Одинаковых свойств для этого мало. Чем структурная типизация отличается от номинальной, часто спрашивают на собеседовании.

У структурной типизации есть обратная сторона. Компилятор не отличит Point от Vec, и если по ошибке передать сдвиг вместо точки, он промолчит. Различать такие значения приходится самому, например по говорящим именам переменных.

Лишние свойства не мешают

У значения может быть и больше свойств, чем требует тип. Брелок на ключе замку не мешает:

TypeScript
interface Point {
    x: number;
    y: number;
}

function show(p: Point): string {
    return `(${p.x}, ${p.y})`;
}

const pin = { x: 1, y: 2, label: "home" };
console.log(show(pin)); // (1, 2)

У pin тип { x: number; y: number; label: string; }: имени нет, есть только форма. В ней есть всё, что требует Point, а лишний label не мешает. Программа печатает (1, 2).

Литерал на месте проверяют строже

Есть одно исключение. Если объектный литерал написан прямо там, где ждут Point, tsc проверяет и лишние свойства. Такой объект больше никто не видит, и лишнее свойство в нём почти наверняка опечатка. Пройдите программу по шагам и сравните строки, где лишнее свойство пришло из переменной и где оно стоит в литерале:

ВживуюЧто подходит под Pointшаг 1 из 6

Константа shift объявлена с типом Vec.

Нажимайте «Следующий шаг»: подсказка покажет тип или ошибку на выделенной строке

Здесь аналогия с замком перестаёт работать. Настоящему замку всё равно, откуда пришёл ключ, а TypeScript к литералу, написанному на месте, строже, чем к значению из переменной.

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

Ждать, что тип параметра отрежет лишние свойства:

TypeScript
interface Point {
    x: number;
    y: number;
}

function save(p: Point): string {
    return JSON.stringify(p);
}

const pin = { x: 1, y: 2, label: "home" };
console.log(save(pin)); // ✗

Компилятор промолчит, и программа напечатает {"x":1,"y":2,"label":"home"}, а не {"x":1,"y":2}. Тип Point только разрешает читать x и y, но сам объект не меняет: типы стираются, и при запуске в p лежит тот же объект с label. Если наружу должны уйти только координаты, соберите новый объект: { x: p.x, y: p.y }.

Главное

  • TypeScript сравнивает типы по форме: значение подходит, если у него есть все нужные свойства нужных типов, а имя типа неважно.
  • Поэтому Vec подходит туда, где ждут Point, и компилятор не отличит одно от другого.
  • Лишние свойства у значения из переменной не мешают; в литерале, написанном на месте, они — ошибка TS2353.
  • Тип не убирает лишние свойства из объекта: при запуске объект остаётся прежним.