В теории типов вариантность описывает отношение между двумя обобщёнными типами (дженериками). Например, в каких обстоятельствах родительский тип может быть заменён дочерним, а в каких — нет, и так далее.
На эту тему можно найти множество ресурсов, особенно таких, где всё описано длинно и сложным, формально-архитектурным языком. Мне бы хотелось создать короткую и простую памятку (с небольшими вкраплениями формализмов), к которой можно легко вернуться, если вдруг забудутся детали.
Ковариантность
Отношение ковариантности представляет собой обычное отношение подтипа, когда более Узкий/Дочерний
тип может использоваться там, где ожидается более Широкий/Родительский
тип. Например:
Я могу поставить Кошку туда, где может стоять любое Животное
Но я не могу поставить любое Животное туда, где может стоять только Кошка
class Animal {
genus: string;
}
class Cat extends Animal {
clawSize: number;
}
function move(animal: Animal) {}
function meow(cat: Cat) {}
move(cat) // Любая кошка может двигаться
meow(animal) // Не каждое животное умеет мяукать
Точнее: Вы можете использовать B там, где ожидается A, если B < A
.
// V — это позиция возвращаемого значения (выход)
type Covariant<V> = () => V;
// Где Animal — широкий тип (W), а Cat — узкий (N)
function covariance(
covW: Covariant<Animal>,
covN: Covariant<Cat>,
) {
covW = covN; // OK. Функция, возвращающая кошку, может заменить функцию, возвращающую животное.
covN = covW; // Ошибка! Нельзя быть уверенным, что функция, возвращающая животное, вернёт именно кошку.
}
Контравариантность
Контравариантность — это противоположность ковариантности. Это, пожалуй, самый сложный для понимания тип вариантности. В случае контравариантности, когда ожидается Узкий/Дочерний
тип, вместо него можно использовать Широкий/Родительский
.
В каких обстоятельствах это может произойти? Представьте себе обработчик
. Например, обработчик общего корма для животных, который обогащает его белком (допустим что это полезно для любого животного). И обработчик
для кошачьего корма, который придаёт ему более рыбный вкус (глупо, но неважно).
Итак, можно ли обработать кошачий корм с помощью общего обработчика корма для животных? Конечно, больше белка кошке не навредит.
А можно ли обработать любой корм для животных с помощью обработчика кошачьего корма? Думаю, нет — не все любят рыбный вкус.
Повторим более формально:
Я могу обработать Кошачий корм так же, как обрабатывается любой корм для Животных.
Но я не могу обработать корм для Животных так же, как обрабатывается Кошачий корм.
class AnimalFood {
protein: number = 0
}
class CatFood extends AnimalFood {
fishness: number = 0
}
function processAnimalFood(animalFood: AnimalFood): void {
// Добавляем немного белка //
}
function processCatFood(catFood: CatFood): void {
// Придаём рыбный вкус //
}
/**
* Перед подачей обработаем корм
*/
function serveAnimalFood(processor: (food: AnimalFood) => void): void {
const food = new AnimalFood();
processor(food);
}
function serveCatFood(processor: (food: CatFood) => void): void {
const food = new CatFood();
processor(food);
}
// Мы не можем использовать обработчик кошачьего корма, чтобы подать корм для животного!
// Не все животные любят рыбный вкус!
serveAnimalFood(processCatFood);
// Вы можете использовать обработчик корма для животных, чтобы подать кошачий корм.
// Белок пойдет кошке на пользу
serveCatFood(processAnimalFood);
В теории типов: Вы можете использовать обработчик для A там, где ожидается обработчик для B, если B < A
.
type Contravariant<V> = (v: V) => void;
// Где Animal — широкий тип (W), а Cat — узкий (N)
function contravariance(
contraW: Contravariant<Animal>,
contraN: Contravariant<Cat>,
) {
contraW = contraN; // Ошибка! Обработчик кошачей еды не может обработать любую еду.
contraN = contraW; // OK! Обработчик общей еды справится и с кошачей.
}
Инвариантность
С инвариантностью всё проще. Это представляет собой отсутствие взаимозаменяемости. В номинативных системах типов, например в С, это единственный вид вариантности. Реальный пример такого отношения можно найти в сортировке мусора.
Есть общее понятие Мусор
и его разновидности, такие как Макулатура
, Пищевые Отходы
и т.д.
И если ваши отходы классифицированы, и для них есть подходящий контейнер, вы должны использовать этот и только этот контейнер.
При сортировке мусора нельзя выбрасывать отходы в общий контейнер, если их можно отсортировать.
Вы можете выбрасывать отходы только в контейнер соответствующего типа.
class Waste {
readonly type = 'неперерабатываемый';
}
class FoodWaste {
readonly type = 'органика';
}
function unrecycledBin(waste: Waste) {}
function organicBin(waste: FoodWaste) {}
unrecycledBin(new FoodWaste()); // Нельзя выбрасывать пищевые отходы в контейнер для неперерабатываемых! Надо быть молодцом!
organicBin(new Waste()); // Нельзя выбрасывать несортированный мусор в контейнер для органики, вы что, преступник???
Формально: Вы можете использовать A только там, где ожидается A
.
type Invariant<V> = (v: V) => V;
function invariance(
inW: Invariant<Animal>,
inN: Invariant<Cat>,
) {
inW = inN; // Ошибка! Типы не взаимозаменяемы.
inN = inW; // Ошибка! То же самое.
}
Бивариантность
Противоположность инвариантности. Бивариантность — это полная взаимозаменяемость, когда тип A
можно заменить на B
и наоборот.
В TypeScript бивариантность не распространена, но всё же встречается. Например, как выяснили ранее, параметры функций являются контравариантны. Но есть исключения: у методов параметры бивариантны.
type Bivariant<V> = {
process(v: V): void;
}
function bivariance(
biW: Bivariant<Animal>,
biN: Bivariant<Cat>,
) {
biW = biN; // OK!
biN = biW; // OK!
}
Такое поведение было выбрано создателями TypeScript для большей гибкости, хотя оно и является теоретически менее строгим. Его можно изменить с помощью явных аннотаций вариантности.
// Ключевое слово `in` в дженериках делает тип Контравариантным
type ContravariantMethod<in V> = {
process(v: V): void;
}
function contravariance(
contraW: ContravariantMethod<Animal>,
contraN: ContravariantMethod<Cat>,
) {
contraW = contraN; // Ошибка! Теперь это строгая контравариантность.
contraN = contraW; // OK!
}
meonsou
Если вы почитаете документацию которую прикрепили то откроете для себя что так делать крайне не желательно. Там это написано выделенным шрифтом специально 5 раз, потому что аннотации вариантности нужны не для этого.
PunGy Автор
Документация предостерегает к необдуманному использованию, это верно. Однако, в случае что я привёл, прямо видно проблему которую создают бивариантные методы. Следующий код, являясь переписанной на классы аналогией секции про контраварианты, ошибку больше не показывает:
TypeScript playground
Так что аннотации вариантности нужны именно для таких случаев. Тут
in
аннотация увеличит безопасность кода и предостережет от ошибки. Для этого они и созданы.meonsou
Давайте вместе прочитаем и переведём документацию:
Никогда не используйте аннотации вариантности которые не совпадают со структурной вариантностью типа.
Аннотации вариантности не изменяют структурную вариантность и проверяются только в специфических ситуациях.
НЕ ИСПОЛЬЗУЙТЕ аннотации вариантности если только они не совпадают со структурным поведением типа.
Не пытайтесь использовать аннотации вариантности чтобы изменить поведение тайпчекера, они не для этого. (Написано дважды)
Буквально пишут что если аннотации вариантности меняют поведение тайпчекера значит они используются неправильно.
meonsou
А вот собственно пример почему их не нужно так использовать (такой тоже есть в документации):
PunGy Автор
И о чём этот пример говорит? Что spread оператор сбрасывает аннотации?
Вот буквально вырезка из этой же документации, которая приводит в пример эту же ситуацию, и прямо заявляет, что "measurement can be inaccurate"
meonsou
Где здесь эта же ситуация? Этот пример не относится к цитате, в примере собственно всё правильно вычисляется и аннотация корректно стоит.
PunGy Автор
Прекрасно сказано: "Никогда не используйте аннотации вариантности которые не совпадают со структурной вариантностью типа".
По вашему мой пример выше это НАРУШЕНИЕ структурной вариантности? Изменение вариантность с бивариантной, которую в области вариантности можно воспринять как
any
, которая в примере выше создают runtime ошибку, на контравариантность - стандартное поведение TypeScript с функциями, что от ошибки избавляет.Какое тогда по вашему мнению правильное использование аннотаций, которое не меняет тайпчекер?
meonsou
В вашем примере структурная и номинальная вариантности не совпадают, это ошибка. От этого предостерегает документация, потому что поведение тайпчекера в таких случаях не определено. Вы выстраиваете зависимость от нестабильной и незадокументированной детали реализации.
Основным применением аннотаций на данный момент считается оптимизация измерения вариантности в тайпчекере для повышения производительности в редакторе. Приходить к выводу что это необходимо следует только после анализа трейсов языкового сервера, выявившего значительную потерю производительности при измерении вариантности.
Так же аннотации могут использоваться в целях документирования вариантности параметров обобщённых типов, но это не совсем удобно потому что их легко можно случайно поставить неправильно.
Ещё у них упоминаются "крайне редкие случаи с рекурсивными типами" когда вариантность измеряется неправильно, но примеров таких случаев я пока лично не видел, это искать надо.