Структурная типизация
Вернёмся к замку. Мастер сделал два ключа с одинаковыми зубцами: на головке одного выбито «Point», на головке другого — «Vec». Замок откроют оба. Надпись на головке он не читает, его интересуют только зубцы.
TypeScript рассуждает так же. Пусть в программе есть точка на карте Point и сдвиг Vec. По смыслу это разные вещи, но устроены они одинаково: у обоих числа x и y. Передадим сдвиг туда, где ждут точку:
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)Давайте разберём пример по строкам:
PointиVec— два разных интерфейса с одинаковыми членами.- Функция
showждётPoint. - Константа
shiftобъявлена какVec, и нигде не сказано, что онаPoint. - Вызов
show(shift)всё равно проходит: уshiftестьxиy, оба числа, — ровно то, что требуетPoint. Программа печатает(3, 4).
Форма, а не имя
Такой способ сравнивать типы называется структурной типизацией: значение подходит под тип, если у него есть все нужные свойства нужных типов. Как называется тип значения и где его объявили, неважно.
Во многих языках, например в Java и C#, правило другое — номинальная типизация. Там Vec подошёл бы туда, где ждут Point, только если бы в объявлении Vec было прямо написано, что это Point, — например, через extends. Одинаковых свойств для этого мало. Чем структурная типизация отличается от номинальной, часто спрашивают на собеседовании.
У структурной типизации есть обратная сторона. Компилятор не отличит Point от Vec, и если по ошибке передать сдвиг вместо точки, он промолчит. Различать такие значения приходится самому, например по говорящим именам переменных.
Лишние свойства не мешают
У значения может быть и больше свойств, чем требует тип. Брелок на ключе замку не мешает:
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 проверяет и лишние свойства. Такой объект больше никто не видит, и лишнее свойство в нём почти наверняка опечатка. Пройдите программу по шагам и сравните строки, где лишнее свойство пришло из переменной и где оно стоит в литерале:
1interface Point {2 x: number;3 y: number;4}5interface Vec {6 x: number;7 y: number;8}9 10const shift: Vec = { x: 3, y: 4 };подсказка редактораshift: Vec11const p: Point = shift;12const pin = { x: 1, y: 2, label: "home" };13const q: Point = pin;14const r: Point = { x: 1, y: 2, size: 5 };
Константа shift объявлена с типом Vec.
Здесь аналогия с замком перестаёт работать. Настоящему замку всё равно, откуда пришёл ключ, а 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. - Тип не убирает лишние свойства из объекта: при запуске объект остаётся прежним.