В теории типов вариантность описывает отношение между двумя обобщёнными типами (дженериками). Например, в каких обстоятельствах родительский тип может быть заменён дочерним, а в каких — нет, и так далее.
На эту тему можно найти множество ресурсов, особенно таких, где всё описано длинно и сложным, формально-архитектурным языком. Мне бы хотелось создать короткую и простую памятку (с небольшими вкраплениями формализмов), к которой можно легко вернуться, если вдруг забудутся детали.
Ковариантность
Отношение ковариантности представляет собой обычное отношение подтипа, когда более Узкий/Дочерний
тип может использоваться там, где ожидается более Широкий/Родительский
тип. Например:
Я могу поставить Кошку туда, где может стоять любое Животное
Но я не могу поставить любое Животное туда, где может стоять только Кошка
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!
}