Модификаторы вариантности параметров типа in и out

Это расширенная функция для решения очень конкретной проблемы, и ее следует использовать только в тех ситуациях, когда вы определили причину ее использования.

Ковариантность и контравариантность — термины теории типов, описывающие взаимосвязь между двумя родовыми типами. Вот краткое описание этой концепции.

Например, если у вас есть интерфейс, представляющий объект, который может иметь определенный член make:

interface Producer<T> {
  make(): T;
}

Мы можем использовать Producer<Cat> там, где ожидается Producer<Animal>a , поскольку Cat — это Animal. Эта связь называется ковариацией : связь от Producer<T> до Producer<U> такая же, как и связь от Tдо U.

И наоборот, если у вас есть интерфейс, который может использовать член consume:

interface Consumer<T> {
  consume: (arg: T) => void;
}

Тогда мы можем использовать Consumer<Animal>там, где ожидается Consumer<Cat>, поскольку любая функция, способная принимать, Animal должна также приниматьCat.

Это отношение называется контравариантностью : отношение от Consumer<T> до Consumer<U> такое же, как отношение от U до T. Обратите внимание на обратный знак по сравнению с ковариацией! Вот почему контравариантность «отменяет сама себя», а ковариация — нет.

interface AnimalProducer {
  make(): Animal;
}

// CatProducer можно использовать везде, где
// ожидается наличие AnimalProducer
interface CatProducer {
  make(): Cat;
}

В TypeScript используется структурная система типов, поэтому при сравнении двух типов, например, чтобы проверить, можно ли использовать Producer<Cat> там, где ожидается Producer<Animal>, обычный алгоритм структурно расширяет оба определения и сравнивает их структуры. Однако дисперсия допускает чрезвычайно полезную оптимизацию: если Producer<T> ковариантен относительно T, то мы можем просто проверить Cat и Animal, поскольку знаем, что они будут иметь те же отношения, что и Producer<Cat> и Producer<Animal>.

Обратите внимание, что эта логика применима только при проверке двух экземпляров одного типа. Если у нас есть Producer<T> и FastProducer<U>, нет гарантии, что T и U обязательно ссылаются на одни и те же позиции в этих типах, поэтому эта проверка всегда будет выполняться структурно.

А теперь к сути дела. В TypeScript все типы проверяются на совместимость по ковариантным правилам, за исключением параметров функций, которые контрвариантны. Поскольку различные правила на сложных рекурсивных типах требуют дорогостоящие вычисления, TypeScript реализует механизм явного аннотирования параметров типа при с помощью необязательных модификаторов in и out .

in указывает, что параметр типа ковариантен, а out контрвариантен. Но стоит сделать акцент на том, что с помощью этих модификаторов невозможно изменить правила по которым TypeScript производит вычисления совместимости, а можно лишь их конкретизировать.

type Setter<T> = (param: T) => void;
type Getter<T> = () => T;

/**
* Стандартный код.
* При сравнении двух сеттеров параметры в сигнатуре
* будут проверятся по контрвариантным правилам,
* а для геттеров возвращаемые типы по ковариантным.
*/
type Setter<in T> = (param: T) => void;
type Getter<out T> = () => T;

/**
* [Код с модификаторами]
* Правила будут идентичны предыдущему примеру.
* Разница лишь в явной конкретизации.
*/

Проще всего воспринимать эти модификаторы, как указание на то, что тип будет использоваться во входных параметрах <in T> или выходных <out T> или и то и другое одновременно <in out T>.

/**
* Указание на то, что тип используется
* во входных и в выходных параметрах.
*/
type Func<in out T> = (param: T) => T;

Поскольку дисперсия — естественное свойство структурных типов, TypeScript автоматически выводит дисперсию каждого обобщенного типа. В крайне редких случаях, связанных с некоторыми видами циклических типов, это измерение может быть неточным. В таком случае можно добавить аннотацию дисперсии к параметру типа, чтобы принудительно задать определённую дисперсию:

// Контравариантная аннотация
interface Consumer<in T> {
  consume: (arg: T) => void;
}

// Контравариантная аннотация
interface Producer<out T> {
  make(): T;
}

// Инвариантная аннотация
interface ProducerConsumer<in out T> {
  consume: (arg: T) => void;
  make(): T;
}

Делайте это только в том случае, если вы пишете ту же дисперсию, которая должна иметь место структурно.

Никогда не пишите аннотацию дисперсии, которая не соответствует структурной дисперсии!

Крайне важно подчеркнуть, что аннотации дисперсии действуют только при сравнении на основе создания экземпляров. Они не действуют при структурном сравнении. Например, аннотации дисперсии нельзя использовать, чтобы «принудительно» сделать тип инвариантным:

// НЕ ДЕЛАЙТЕ ЭТОГО - аннотация дисперсии
// не соответствует структурному поведению
interface Producer<in out T> {
  make(): T;
}

// Это не ошибка типа, а структурная ошибка
// сравнения, поэтому аннотации дисперсии
// не действует
const p: Producer<string | number> = {
    make(): number {
        return 42;
    }
}

Здесь make функция литерала объекта возвращает number, что могло бы вызвать ошибку, поскольку number не является string | number. Однако это не сравнение на основе создания экземпляра, поскольку литерал объекта — анонимный тип, а не Producer<string | number>.

Аннотации дисперсии не изменяют структурное поведение и используются только в определенных ситуациях.

Крайне важно писать аннотации вариативности только в том случае, если вы точно знаете, зачем это делаете, каковы их ограничения и когда они не действуют. Использование TypeScript сравнения на основе создания экземпляров или структурного сравнения не является чётким и может меняться от версии к версии в целях обеспечения корректности или производительности, поэтому аннотации вариативности следует писать только тогда, когда они соответствуют структурному поведению типа. Не используйте аннотации вариативности, чтобы «навязать» определённую вариативность; это приведёт к непредсказуемому поведению кода.

НЕ пишите аннотации вариантов, если они не соответствуют структурному поведению типа.

Помните, TypeScript может автоматически выводить вариантность из ваших обобщённых типов. Практически никогда не требуется писать аннотацию вариантности, и делать это следует только при наличии конкретной потребности. Аннотации вариантности не изменяют структурное поведение типа, и в зависимости от ситуации вы можете увидеть структурное сравнение, хотя ожидали сравнения на основе создания экземпляра. Аннотации вариантности не могут использоваться для изменения поведения типов в этих структурных контекстах и ​​не должны создаваться, если аннотация не совпадает со структурным определением. Поскольку это сложно сделать правильно, а TypeScript может правильно выводить вариантность в подавляющем большинстве случаев, вам не следует писать аннотации вариантности в обычном коде.

Не пытайтесь использовать аннотации дисперсии для изменения поведения проверки типов; они не для этого предназначены.

Временные аннотации дисперсии могут оказаться полезными при отладке типов, поскольку они проверяются. TypeScript выдаст ошибку, если аннотированная дисперсия явно неверна:

// Ошибка, эт��т интерфейс определенно контравариантен на T
interface Foo<out T> {
  consume: (arg: T) => void;
}

Однако аннотации дисперсии могут быть более строгими (например, in out допустимы, если фактическая дисперсия ковариантна). Обязательно удалите аннотации дисперсии после завершения отладки.

Наконец, если вы пытаетесь максимизировать производительность проверки типов, запустили профилировщик и определили конкретный медленный тип, а также определили, что вывод дисперсии является именно медленным, и тщательно проверили аннотацию дисперсии, которую вы хотите написать, вы можете заметить небольшое повышение производительности в чрезвычайно сложных типах, добавив аннотации дисперсии.

Не пытайтесь использовать аннотации дисперсии для изменения поведения проверки типов; они не для этого предназначены.

Комментарии (1)


  1. PaulIsh
    17.11.2025 01:46

    Статья говорит, что in/our есть, но использовать их не нужно. А если нужно, то использовать с осторожностью. Так если они всё-таки нужны, то хотелось бы реальные примеры где они помогают.