Совместимость типов
Совместимость типов в TypeScript основана на структурном подтипировании. Структурная типизация — это способ установления связи между типами, основанный исключительно на их членах. Это отличается от номинальной типизации. Рассмотрим следующий код:
interface Pet {
name: string;
}
class Dog {
name: string;
}
let pet: Pet;
// OK, because of structural typing
pet = new Dog(); В языках с номинальной типизацией, таких как C# или Java, эквивалентный код будет ошибкой, потому что класс Dog не описывает себя явно как реализующий интерфейс Pet.
Структурная система типов TypeScript была разработана с учётом того, как обычно пишут код на JavaScript. Поскольку JavaScript широко использует анонимные объекты, такие как выражения функций и литералы объектов, гораздо естественнее представлять типы отношений, встречающиеся в JavaScript-библиотеках, с помощью структурной, а не номинальной системы типов.
Примечание о корректности
Система типов TypeScript позволяет некоторые операции, безопасность которых нельзя гарантировать на этапе компиляции. Если система типов обладает таким свойством, то она считается «некорректной». Места, где TypeScript допускает некорректное поведение, были тщательно продуманы, и в этом документе мы объясним, где это происходит и какие сценарии его мотивируют.
Начало работы
Основное правило структурной системы типов TypeScript заключается в том, что x совместим с y, если y имеет по меньшей мере такие же члены, что и x. Например, рассмотрим следующий код, включающий интерфейс с именем Pet, имеющий свойство name:
interface Pet {
name: string;
}
let pet: Pet;
// dog's inferred type is { name: string; owner: string; }
let dog = { name: "Lassie", owner: "Rudd Weatherwax" };
pet = dog; Для проверки того, можно ли присвоить dog к pet, компилятор проверяет каждое свойство pet в поисках соответствующего совместимого свойства в dog. В этом случае dog должен иметь член под названием name, являющийся строкой. Он есть, поэтому присвоение разрешено.
То же правило присвоения используется при проверке аргументов вызова функций:
interface Pet {
name: string;
}
let dog = { name: "Lassie", owner: "Rudd Weatherwax" };
function greet(pet: Pet) {
console.log("Hello, " + pet.name);
}
greet(dog); // OK Обратите внимание, что dog имеет дополнительное свойство owner, но это не создает ошибки. При проверке совместимости учитываются только члены целевого типа (в данном случае Pet). Этот процесс сравнения происходит рекурсивно, исследуя тип каждого члена и подчлена.
Однако следует помнить, что литералы объектов могут содержать только известные свойства. Например, поскольку мы явно указали, что dog имеет тип Pet, следующий код некорректен:
let dog: Pet = { name: "Lassie", owner: "Rudd Weatherwax" }; // Error Сравнение двух функций
Хотя сравнение примитивных типов и типов объектов относительно просто, вопрос о том, какие функции следует считать совместимыми, несколько сложнее. Начнем с простого примера двух функций, отличающихся только списками параметров:
let x = (a: number) => 0; let y = (b: number, s: string) => 0; y = x; // OK x = y; // Error
Чтобы проверить, может ли x быть присвоено y, мы сначала смотрим на список параметров. Каждый параметр в x должен иметь соответствующий параметр в y с совместимым типом. Обратите внимание, что имена параметров не учитываются, только их типы. В этом случае каждый параметр x имеет соответствующий совместимый параметр в y, поэтому присвоение разрешено.
Второе присвоение является ошибкой, так как y имеет обязательный второй параметр, которого у x нет, поэтому присвоение запрещено.
Вы, возможно, спросите, почему мы допускаем «отбрасывание» параметров, как в примере y = x. Причина, по которой это присвоение разрешено, заключается в том, что игнорирование дополнительных параметров функции в JavaScript довольно часто встречается. Например, Array#forEach предоставляет три параметра функции обратного вызова: элемент массива, его индекс и содержащий массив. Тем не менее, очень полезно предоставить функцию обратного вызова, использующую только первый параметр:
let items = [1, 2, 3]; // Don't force these extra parameters items.forEach((item, index, array) => console.log(item)); // Should be OK! items.forEach((item) => console.log(item));
Теперь давайте посмотрим, как обрабатываются возвращаемые типы, используя две функции, отличающиеся только возвращаемым типом:
let x = () => ({ name: "Alice" });
let y = () => ({ name: "Alice", location: "Seattle" });
x = y; // OK
y = x; // Error, because x() lacks a location property Система типов требует, чтобы возвращаемый тип исходной функции был подтипом возвращаемого типа целевого типа.
Бивариантность параметров функций
При сравнении типов параметров функций присвоение выполняется, если либо исходный параметр может быть присвоен целевому параметру, либо наоборот. Это некорректно, потому что вызывающая сторона может получить функцию, принимающую более специализированный тип, но вызывать функцию с менее специализированным типом. На практике такие ошибки встречаются редко, а разрешение этого позволяет использовать многие распространённые JavaScript-паттерны. Краткий пример:
enum EventType {
Mouse,
Keyboard,
}
interface Event {
timestamp: number;
}
interface MyMouseEvent extends Event {
x: number;
y: number;
}
interface MyKeyEvent extends Event {
keyCode: number;
}
function listenEvent(eventType: EventType, handler: (n: Event) => void) {
/* ... */
}
// Unsound, but useful and common
listenEvent(EventType.Mouse, (e: MyMouseEvent) => console.log(e.x + "," + e.y));
// Undesirable alternatives in presence of soundness
listenEvent(EventType.Mouse, (e: Event) =>
console.log((e as MyMouseEvent).x + "," + (e as MyMouseEvent).y)
);
listenEvent(EventType.Mouse, ((e: MyMouseEvent) =>
console.log(e.x + "," + e.y)) as (e: Event) => void);
// Still disallowed (clear error). Type safety enforced for wholly incompatible types
listenEvent(EventType.Mouse, (e: number) => console.log(e)); Вы можете заставить TypeScript генерировать ошибки в таких случаях, используя флаг компилятора strictFunctionTypes.
Необязательные параметры и параметры rest
При сравнении функций на совместимость необязательные и обязательные параметры взаимозаменяемы. Дополнительные необязательные параметры исходного типа не являются ошибкой, а необязательные параметры целевого типа без соответствующих параметров в исходном типе не являются ошибкой.
Когда функция имеет параметр rest, она обрабатывается как бесконечная последовательность необязательных параметров.
Это некорректно с точки зрения системы типов, но с точки зрения выполнения идея необязательного параметра обычно не обеспечивается надёжно, поскольку передача undefined в этой позиции эквивалентна для большинства функций.
Мотивирующим примером является распространённый паттерн функции, которая принимает функцию обратного вызова и вызывает её с определённым (для программиста), но неизвестным (для системы типов) количеством аргументов:
function invokeLater(args: any[], callback: (...args: any[]) => void) {
/* ... Invoke callback with 'args' ... */
}
// Unsound - invokeLater "might" provide any number of arguments
invokeLater([1, 2], (x, y) => console.log(x + ", " + y));
// Confusing (x and y are actually required) and undiscoverable
invokeLater([1, 2], (x?, y?) => console.log(x + ", " + y)); Функции с перегрузками
Когда функция имеет перегрузки, каждая перегрузка в целевом типе должна иметь соответствующую совместимую сигнатуру в исходном типе. Это гарантирует, что исходная функция может вызываться во всех тех же случаях, что и целевая функция.
Перечисления
Перечисления совместимы с числами, а числа совместимы с перечислениями. Значения перечислений из разных типов перечислений считаются несовместимыми. Например,
enum Status {
Ready,
Waiting,
}
enum Color {
Red,
Blue,
Green,
}
let status = Status.Ready;
status = Color.Green; // Error Классы
Классы работают аналогично литералам объектов и интерфейсам с одним исключением: они имеют как статический, так и экземплярный тип. При сравнении двух объектов типа класса сравниваются только члены экземпляра. Статические члены и конструкторы не влияют на совместимость.
class Animal {
feet: number;
constructor(name: string, numFeet: number) {}
}
class Size {
feet: number;
constructor(numFeet: number) {}
}
let a: Animal;
let s: Size;
a = s; // OK
s = a; // OK Приватные и защищённые члены в классах
Приватные и защищённые члены в классе влияют на их совместимость. Когда экземпляр класса проверяется на совместимость, если целевой тип содержит приватный член, то исходный тип также должен содержать приватный член, происходящий из того же класса. Аналогично, это же относится к экземпляру с защищённым членом. Это позволяет классу быть совместимым по присвоению со своим суперклассом, но не с классами из другой иерархии наследования, которые в остальном имеют одинаковую структуру.
Обобщения
Поскольку TypeScript — это структурная система типов, параметры типов влияют на результирующий тип только тогда, когда они используются как часть типа члена. Например,
interface Empty<T> {}
let x: Empty<number>;
let y: Empty<string>;
x = y; // OK, because y matches structure of x В приведенном выше примере x и y совместимы, так как их структуры не используют аргумент типа для различения. Изменение этого примера добавлением члена в Empty<T> показывает, как это работает:
interface NotEmpty<T> {
data: T;
}
let x: NotEmpty<number>;
let y: NotEmpty<string>;
x = y; // Error, because x and y are not compatible Таким образом, обобщённый тип, у которого указаны его аргументы типа, ведет себя как необобщённый тип.
Для обобщённых типов, у которых не указаны их аргументы типа, совместимость проверяется путём указания any вместо всех не указанных аргументов типа. Полученные типы проверяются на совместимость, как и в необобщённом случае.
Например,
let identity = function <T>(x: T): T {
// ...
};
let reverse = function <U>(y: U): U {
// ...
};
identity = reverse; // OK, because (x: any) => any matches (y: any) => any Дополнительные темы
Подтип против присвоения
До сих пор мы использовали термин «совместимый», который не определён в спецификации языка. В TypeScript существует два вида совместимости: подтип и присвоение. Они отличаются только тем, что присвоение расширяет совместимость подтипов, позволяя присваивать значения типам any и типам enum с соответствующими числовыми значениями.
Разные части языка используют один из двух механизмов совместимости в зависимости от ситуации. В практическом плане совместимость типов определяется совместимостью по присвоению, даже в случаях implements и extends.
Совместимость присвоения типов any, unknown, object, void, undefined, null и never
В следующей таблице суммируется совместимость присвоения между некоторыми абстрактными типами. Строки показывают, к чему можно присвоить каждый из них, столбцы показывают, к чему каждый может быть присвоен. «✓» указывает на сочетание, совместимое только при выключенном strictNullChecks.
| любой | неизвестный | объект | пустое | неопределённый | null | никогда | |
|---|---|---|---|---|---|---|---|
| любой → | ✓ | ✓ | ✓ | ✓ | ✓ | ✕ | |
| неизвестный → | ✓ | ✕ | ✕ | ✕ | ✕ | ✕ | |
| объект → | ✓ | ✓ | ✕ | ✕ | ✕ | ✕ | |
| пустое → | ✓ | ✓ | ✕ | ✕ | ✕ | ✕ | |
| неопределённый → | ✓ | ✓ | ✓ | ✓ | ✓ | ✕ | |
| null → | ✓ | ✓ | ✓ | ✓ | ✓ | ✕ | |
| никогда → | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
Повторяя Основы:
- Всё присваивается самому себе.
-
anyиunknownодинаковы с точки зрения присваивания, но отличаются тем, чтоunknownне присваивается ничему, кромеany. -
unknownиneverкак инверсии друг друга. Всё присваиваетсяunknown, аneverприсваивается всем. Ничего не присваиваетсяnever, аunknownне присваивается ничему (кромеany). -
voidне присваивается ничему, за исключением следующих случаев:any,unknown,never,undefined, иnull(еслиstrictNullChecksвыключен, см. таблицу для деталей). - Когда
strictNullChecksвыключен,nullиundefinedпохожи наnever: присваиваются большинству типов, но большинство типов не присваиваются им. Они присваиваются друг другу. - Когда
strictNullChecksвключен,nullиundefinedведут себя больше какvoid: не присваиваются ничему, кромеany,unknown,never, иvoid(undefinedвсегда присваиваетсяvoid).
© 2012-2023 Microsoft
Licensed under the Apache License, Version 2.0.
https://www.typescriptlang.org/docs/handbook/type-compatibility.html