← Курс/Ковариантность и контравариантность#178 из 257+35 XP

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

Что такое вариантность

Вариантность описывает, как совместимость типов «передаётся» через параметры дженериков. Вопрос: если Dog extends Animal, то как соотносятся Box<Dog> и Box<Animal>?

class Animal { breathe() {} }
class Dog extends Animal { bark() {} }
class Cat extends Animal { meow() {} }

// Dog — подтип Animal (Dog extends Animal)

Ковариантность (Covariance) — возвращаемые типы

**Ковариантный** означает: если Dog extends Animal, то Producer<Dog> совместим с Producer<Animal>.

// Producer<T> — только возвращает T (out-позиция)
type Producer<T> = { produce(): T }

const dogProducer: Producer<Dog> = { produce: () => new Dog() }
const animalProducer: Producer<Animal> = dogProducer  // OK! Ковариантно

// Почему безопасно? Если мы ожидаем Animal, а получаем Dog — всё норм.
// Dog является Animal.

Контравариантность (Contravariance) — параметры функций

**Контравариантный** означает: если Dog extends Animal, то Consumer<Animal> совместим с Consumer<Dog>.

// Consumer<T> — только принимает T (in-позиция)
type Consumer<T> = { consume(value: T): void }

const animalConsumer: Consumer<Animal> = { consume: (a) => a.breathe() }
const dogConsumer: Consumer<Dog> = animalConsumer  // OK! Контравариантно

// Почему безопасно? Consumer<Animal> принимает любое Animal.
// Dog — это Animal, значит он тоже будет принят.

// Наоборот НЕБЕЗОПАСНО:
// const animalConsumer2: Consumer<Animal> = dogConsumer  // Ошибка!
// Если подставить Cat (тоже Animal), но Consumer<Dog> ожидает Dog.bark() — ошибка!

Биварантность в методах (исторический артефакт)

В TypeScript методы классов и объектов исторически бивариантны — они принимают и супертипы, и подтипы в позиции параметра:

interface A {
  method(x: string): void  // бивариантно (для обратной совместимости)
  fn: (x: string) => void  // контравариантно со strictFunctionTypes
}

При включённом strictFunctionTypes: true функции в позиции свойств (не методов) становятся контравариантными.

Ключевые слова in и out (TypeScript 4.7+)

// out — ковариантный параметр (только возвращается)
type Producer<out T> = {
  produce(): T
  // consume(value: T): void  // Ошибка TS: out позиция, нельзя в параметре
}

// in — контравариантный параметр (только принимается)
type Consumer<in T> = {
  consume(value: T): void
  // produce(): T  // Ошибка TS: in позиция, нельзя в возвращаемом типе
}

// in out — инвариантный (и принимается, и возвращается)
type Transformer<in out T> = {
  transform(value: T): T
}

Практический пример: функции высшего порядка

// Зная вариантность, понимаем почему работает код:

function handleAnimal(handler: (animal: Animal) => void, dog: Dog) {
  handler(dog)  // OK — Dog является Animal
}

const logAnimal = (animal: Animal) => animal.breathe()
const logDog    = (dog: Dog)       => dog.bark()

handleAnimal(logAnimal, new Dog())  // OK — ковариантно по собаке
// handleAnimal(logDog, new Dog())  // Ошибка! handler ожидает Animal, но logDog хочет bark()

Примеры

Демонстрация ковариантности и контравариантности через runtime примеры: безопасные и небезопасные замены типов

// Вариантность — теоретическая концепция TypeScript,
// но мы можем показать её принципы через runtime-примеры.

// Иерархия типов:
// Animal ← Dog ← GoldenRetriever

class Animal {
  constructor(name) {
    this.name = name
  }
  breathe() {
    return `${this.name} дышит`
  }
  toString() {
    return `Animal(${this.name})`
  }
}

class Dog extends Animal {
  bark() {
    return `${this.name} лает: Гав!`
  }
}

class GoldenRetriever extends Dog {
  fetch() {
    return `${this.name} принёс мяч!`
  }
}

class Cat extends Animal {
  meow() {
    return `${this.name} мяукает`
  }
}

// Ковариантность: Producer<Dog> совместим с Producer<Animal>
// Производители возвращают тип — можно идти "вверх по иерархии"

class DogProducer {
  produce() { return new Dog('Rex') }
}

class AnimalProducer {
  produce() { return new Animal('Generic') }
}

// В TypeScript: const p: Producer<Animal> = new DogProducer()  — OK (ковариантно)
function useAnimalProducer(producer) {
  const animal = producer.produce()
  // Мы ожидаем Animal — Dog тоже Animal, всё безопасно
  console.log(animal.breathe())
}

console.log('=== Ковариантность (Producer) ===')
useAnimalProducer(new AnimalProducer())  // OK — Animal
useAnimalProducer(new DogProducer())     // OK — Dog тоже Animal (ковариантно)

// Контравариантность: Consumer<Animal> совместим с Consumer<Dog>
// Потребители принимают тип — можно идти "вниз по иерархии"

const consumeAnimal = (animal) => {
  // Работает с любым Animal
  console.log(`Обработка животного: ${animal.breathe()}`)
}

const consumeDog = (dog) => {
  // Требует Dog — вызывает bark()
  console.log(`Обработка собаки: ${dog.bark()}`)
}

console.log('\n=== Контравариантность (Consumer) ===')
function useDogConsumer(consumer) {
  const dog = new Dog('Buddy')
  // Передаём Dog в consumer
  consumer(dog)
}

// Безопасно: consumeAnimal работает с любым Animal (в т.ч. Dog)
useDogConsumer(consumeAnimal)  // OK — контравариантно

// НЕБЕЗОПАСНО: consumeDog требует Dog.bark(), но может получить Cat
function useAnimalConsumer(consumer) {
  const cat = new Cat('Whiskers')
  // Опасность: если consumer ожидает Dog, а получает Cat — bark() не существует!
  try {
    consumer(cat)
  } catch (e) {
    console.log(`Ошибка контравариантности: ${e.message}`)
  }
}

console.log('\n=== Небезопасная замена (нарушение контравариантности) ===')
useAnimalConsumer(consumeDog)  // ОШИБКА: cat.bark не является функцией

// Инвариантность: Transformer<Dog> ≠ Transformer<Animal>
// Если тип и читается, и записывается — замена невозможна

class AnimalTransformer {
  transform(animal) {
    animal.isTransformed = true
    return animal
  }
}

class DogTransformer {
  transform(dog) {
    dog.isTransformed = true
    dog.bark()  // требует Dog!
    return dog
  }
}

console.log('\n=== Инвариантность (Transformer) ===')
const at = new AnimalTransformer()
const dog = new Dog('Max')
console.log(at.transform(dog).breathe())  // OK — принимает любой Animal

const dt = new DogTransformer()
try {
  const cat = new Cat('Felix')
  dt.transform(cat)  // Опасно — cat не имеет bark()
} catch (e) {
  console.log('Ошибка инвариантности:', e.message)
}

console.log('\n=== Безопасная цепочка (ковариантность функций) ===')
// Функция принимает широкий тип, возвращает узкий — безопасно
const animalToString = (a) => a.breathe()   // Animal → string
const dogToString    = (d) => d.bark()       // Dog → string

// map ковариантен по возвращаемому типу
const animals = [new Dog('Rex'), new GoldenRetriever('Buddy')]
console.log(animals.map(animalToString))  // OK — используем широкий предикат