Кратко о вариантности с примерами на TypeScript

от автора

В теории типов вариантность описывает отношение между двумя обобщёнными типами (дженериками). Например, в каких обстоятельствах родительский тип может быть заменён дочерним, а в каких — нет, и так далее.

На эту тему можно найти множество ресурсов, особенно таких, где всё описано длинно и сложным, формально-архитектурным языком. Мне бы хотелось создать короткую и простую памятку (с небольшими вкраплениями формализмов), к которой можно легко вернуться, если вдруг забудутся детали.

Ковариантность

Отношение ковариантности представляет собой обычное отношение подтипа, когда более Узкий/Дочерний тип может использоваться там, где ожидается более Широкий/Родительский тип. Например:

Я могу поставить Кошку туда, где может стоять любое Животное
Но я не могу поставить любое Животное туда, где может стоять только Кошка

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 {     // } class CatFood extends AnimalFood {     // } 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! } 

Ссылки


ссылка на оригинал статьи https://habr.com/ru/articles/944074/