Разница между ковариантными и контравариантными позициями в Typescript - PullRequest
2 голосов
/ 21 июня 2020

Я пытаюсь понять следующие примеры из Справочника по расширенным типам Typescript .

Цитата:

Следующий пример демонстрирует, как несколько кандидатов для переменной того же типа в ковариантных позициях приводит к выводу типа объединения:

type Foo<T> = T extends { a: infer U, b: infer U } ? U : never;
type T10 = Foo<{ a: string, b: string }>;  // string
type T11 = Foo<{ a: string, b: number }>;  // string | number

Аналогичным образом, несколько кандидатов на переменную одного и того же типа в противоположных позициях приводит к выводу типа пересечения:

type Bar<T> = T extends { a: (x: infer U) => void, b: (x: infer U) => void } ? U : never;
type T20 = Bar<{ a: (x: string) => void, b: (x: string) => void }>;  // string
type T21 = Bar<{ a: (x: string) => void, b: (x: number) => void }>;  // string & number

У меня вопрос: почему свойства объекта из первого примера считаются «ковариантными позициями», а аргументы второй функции считаются «противоположными позициями»?

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

1 Ответ

1 голос
/ 21 июня 2020

Ваше наблюдение, что один из примеров разрешается до never, является точным, и вы не пропустили никаких настроек компилятора. В более новых версиях TS пересечения примитивных типов разрешаются в never. Если вы вернетесь к старой версии , вы все равно увидите string & number. В более новой версии вы все еще можете увидеть контравариантное поведение позиции, если используете типы объектов:

type Bar<T> = T extends { a: (x: infer U) => void, b: (x: infer U) => void } ? U : never;
type T21 = Bar<{ a: (x: { h: string }) => void, b: (x: { g: number }) => void }>;  // {h: string; } & { g: number;}

Ссылка на игровую площадку

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

Для аргументов функции легко понять, почему они будут контравариантными. Вы можете безопасно вызывать функцию только с подтипом аргумента, но не с базовым типом.

class Animal { eat() { } }
class Dog extends Animal { wof() { } }

type Fn<T> = (p: T) => void
var contraAnimal: Fn<Animal> = a => a.eat();
var contraDog: Fn<Dog> = d => { d.eat(); d.wof() }
contraDog(new Animal()) // error, d.wof would fail 
contraAnimal = contraDog; // so this also fails

contraAnimal(new Dog()) // This is ok
contraDog = contraAnimal; // so this is also ok 

Ссылка на игровую площадку

Поскольку Fn<Animal> и Fn<Dog> присваиваются в противоположном направлении, как две переменные типов Dog и Animal, позиция параметра функции делает Fn контравариантным в T

Для свойств обсуждение того, почему они ковариантность немного сложнее. TL / DR заключается в том, что позиция поля (например, { a: T }) сделает тип фактически инвариантным, но это усложнит жизнь, поэтому в TS по определению позиция типа поля (например, T имеет выше) делает тип ковариантно в этом типе поля (поэтому { a: T } ковариантно в T). Мы могли бы продемонстрировать, что для a только случайный регистр { a: T } будет ковариантным, а для a только записываемым регистр { a: T } будет контравариантным, и оба случая вместе дают нам инвариантность, но я не уверен, что строго необходимо, вместо этого я оставляю вас с этим примером, где это ковариантное поведение по умолчанию может привести к правильно набранному коду с ошибками времени выполнения:

type SomeType<T> = { a: T }

function foo(a: SomeType<{ foo: string }>) {
    a.a = { foo: "" } // no bar here, not needed
}
let b: SomeType<{ foo: string, bar: number }> = {
    a: { foo: "", bar: 1 }
}

foo(b) // valid T is in a covariant position, so SomeType<{ foo: string, bar: number }> is assignable to SomeType<{ foo: string }>
b.a.bar.toExponential() // Runtime error nobody in foo assigned bar

Ссылка на игровую площадку

Вы также можете найти этот мой пост о дисперсии в TS.

...