typescript - Typescript中协变和逆变位置的区别

标签 typescript covariance

我试图从 Typescript advanced types handbook 中理解以下示例.
引用,它说:
以下示例演示了协变位置中同一类型变量的多个候选者如何导致推断联合类型:

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
我的问题是:为什么第一个示例中的对象属性被视为“协变位置”,而第二个函数参数被视为“逆变位置”?
第二个例子似乎也解析为 never不确定是否需要任何配置才能使其工作。

最佳答案

您观察到其中一个示例解析为 never是准确的,您不会遗漏任何编译器设置。在较新版本的 TS 中,原始类型的交集解析为 never .如果您恢复到 older version您仍然会看到 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;}
Playground Link
至于为什么函数参数是逆变的而属性是协变的,这是类型安全性和可用性之间的权衡。
对于函数参数,很容易看出为什么它们是逆变的。您只能使用参数的子类型而不是基类型安全地调用函数。
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 

Playground Link
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

Playground Link
您可能还会找到 this我关于 TS 中方差的帖子很有趣。

关于typescript - Typescript中协变和逆变位置的区别,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/62496072/

相关文章:

reactjs - TypeScript React 中可重用的布局组件 : Property 'path' does not exist on type 'IntrinsicAttributes & IProps'

typescript - 如何从接口(interface)中省略接口(interface)

php - PHP 中抽象类和特征的类型协变

c# - 在没有 .ToList() 复制操作的情况下将 List<Concrete> 转换为 List<Inherited Interface>

c# - 从 Dictionary<int, Fleas> 中高效获取 IReadOnlyDictionary<int, Animals>

java - 我的通用树实现中的泛型和子类型问题

angular - ngx-translate 如何测试组件

typescript - 如何根据 TypeScript 中的其他类型使对象属性可选?

javascript - react : How to mock Auth0 for testing with Jest

Swift 在协议(protocol)中使用泛型