Ваше наблюдение, что один из примеров разрешается до 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.