在TypeScript中,Variance、Covariance、Contravariance、Bivariance和Invariance之间的区别是什么? Variance(变异性)是指类型在继承关系中的变化。Covariance(协变性)表示子类型的关系可以保持或扩大父类型的关系。Contravariance(逆变性)表示子类型的关系可以缩小或反转父类型的关系。Bivariance(双变性)是指类型可以同时具有协变性和逆变性。Invariance(不变性)表示类型在继承关系中保持不变。 在TypeScript中,这些概念主要与函数参数和返回类型有关。使用协变性,可以将子类型的函数赋值给父类型的函数,而不会引发类型错误。使用逆变性,可以将父类型的函数赋值给子类型的函数,同样不会引发类型错误。另外,使用双变性可以在某些情况下同时兼容协变性和逆变性。 了解这些概念对于正确理解和使用TypeScript中的类型系统非常重要。根据具体的需求和场景,选择正确的类型变异方式可以提高代码的可读性和可维护性。

32
请用简单明了的TypeScript示例解释一下什么是Variance(变异性)、Covariance(协变性)、Contravariance(逆变性)、Bivariance(双变性)和Invariance(不变性)?

@jonrsharpe,你能否请撤销你的更改? - captain-yossarian from Ukraine
不,帖子的问题部分是为了提出“问题”,而不仅仅是草稿本。如果您想写一个额外的答案(到目前为止,您发布的内容只是链接),请将其作为“答案”进行。 - jonrsharpe
1
@jonrsharpe 啊,好的,你说得对,但我太懒了不想复制这些链接。对于感兴趣的人:你可以查看此问题的编辑历史并复制相关链接。 - captain-yossarian from Ukraine
2个回答

54

Variance涉及到泛型类型F<T>相对于其类型参数T的变化。如果你知道T extends U,那么方差将告诉你是否可以得出F<T> extends F<U>的结论,得出F<U> extends F<T>的结论,或者两者都不是。


协变性意味着F<T>T共变的。也就是说,F<T>T 同向变化。换句话说,如果T extends U,那么F<T> extends F<U>。例如:

  • Function or method types co-vary with their return types:

    type Co<V> = () => V;
    function covariance<U, T extends U>(t: T, u: U, coT: Co<T>, coU: Co<U>) {
      u = t; // okay
      t = u; // error!
    
      coU = coT; // okay
      coT = coU; // error!
    }
    

其他(目前未进行图示)的例子包括:

  • 对象在其属性类型方面是协变的,尽管对于可变属性来说这听起来不太合理
  • 类构造函数在其实例类型方面是协变的

{{逆变}}意味着F<T>T-。也就是说,F<T>T反向变化(方向相反)。换句话说,如果T extends U,那么F<U> extends F<T>。例如:
  • Function types contra-vary with their parameter types (with --strictFunctionTypes enabled):

    type Contra<V> = (v: V) => void;
    function contravariance<U, T extends U>(t: T, u: U, contraT: Contra<T>, contraU: Contra<U>) {
      u = t; // okay
      t = u; // error!
    
      contraU = contraT; // error!
      contraT = contraU; // okay
    }
    

其他(暂未说明)的例子包括:

  • 对象在其键类型上是逆变的
  • 类构造函数在其构造参数类型上是逆变的

不变性意味着F<T>既不随T变化也不反对T:在T中,F<T>既不是协变的也不是逆变的。实际上,在最一般的情况下,这就是发生的情况。协变和逆变是“脆弱”的,因为当你结合协变和逆变类型函数时,很容易产生不变的结果。例如:

  • Function types that return the same type as their parameter neither co-vary nor contra-vary in that type:

    type In<V> = (v: V) => V;
    function invariance<U, T extends U>(t: T, u: U, inT: In<T>, inU: In<U>) {
      u = t; // okay
      t = u; // error!
    
      inU = inT; // error!
      inT = inU; // error!
    }
    

Bivariance 意味着 F<T>T 上同时是协变和逆变的: F<T>T 上既不是协变也不是逆变。在一个良好的类型系统中,对于任何非平凡的类型函数,这基本上是不可能发生的。您可以证明只有一个常量类型函数,例如 type F<T> = string 真正是双变量的(快速草图:对于所有的 TT extends unknown 都成立,因此 F<T> extends F<unknown>F<unknown> extends T,在一个良好的类型系统中,如果 A extends BB extends A,那么 A 就等同于 B。因此,如果对于所有的 TF<T> = F<unknown>,则 F<T> 是常量)。
但是 TypeScript 并没有 也没有打算拥有 完全良好的类型系统。并且有 一个显著的例子,TypeScript 将类型函数视为双变量:
  • Method types both co-vary and contra-vary with their parameter types (this also happens with all function types with --strictFunctionTypes disabled):

    type Bi<V> = { foo(v: V): void };
    function bivariance<U, T extends U>(t: T, u: U, biT: Bi<T>, biU: Bi<U>) {
      u = t; // okay
      t = u; // error!
    
      biU = biT; // okay
      biT = biU; // okay
    }
    

代码操场链接


1
这是一个非常棒的令人费解的练习! - Qwerty

3

A.S.:jcalz的回答从技术角度来看非常好。我想补充一些直观的解释。

方差何时相关?

当你处理两种类型既不完全相同,也不完全无关的情况时,方差的概念变得有用。

原因如下。

在这个例子中,value1value2都是相同类型的number,因此一个总是可以赋值给另一个,反之亦然,没有任何错误

declare let value1: number // for example: 42, or -3, or NaN, etc.
declare let value2: number // for example: 17, or Math.PI, or Infinity, etc.

value1 = value2 // no error
value2 = value1 // no error

相反地,在这里,value1 是一个number,而value2 是一个string。这些类型彼此之间没有任何关联,因此将一个赋值给另一个总是错误的。
declare let value1: number // for example: 42, or 17, etc.
declare let value2: string // for example: "Hello world", "Lorem ipsum", etc.

value1 = value2 // Error!
value2 = value1 // Error!

在这两种情况下,无论是赋值方向(从value2value1还是相反),结果总是一样的:第一种情况总是没有错误,而第二种情况总是有错误。这就是为什么这里没有涉及到变异性。
当你有两种类型,它们有些相似但不完全相同时,赋值的顺序通常很重要:将一个值赋给另一个是可以的,但反过来却不行。
让我们看一个例子。
在这里,类型Person只有一个属性name,而类型Student定义了namegraduationYear两个属性。也就是说,Student包含了Person的所有内容,而Person只部分涵盖了Student将一个人分配给一个学生是错误的,因为一个学生应该有一个毕业年份,但Person只有name属性。然而,将一个学生分配给一个人是完全可以的,因为一个学生,就像任何一个人一样,都有一个名字(无论是否有其他属性)。
type Person = { name: string }
type Student = { name: string, graduationYear: number }

declare let person: Person // for example: { name: 'Mike' }
declare let student: Student // for example: { name: 'Sofia', graduationYear: 2020 }

person = student // no error
student = person // Error!

这还不是方差。
方差是衡量给定泛型的实例之间的可分配性与其类型参数的实例之间的可分配性之间的相关性的一种度量。

好的,让我们来解开这个问题。
一个泛型是通过另一个类型来定义的类型。一个很好的例子是Array:即使两者都是数组,Array<number>Array<string>并不相同。在Array<number>中的numberArray<string>中的string是泛型类型Array<…>的类型参数(或类型参数)。
任何值都可以是数组的项(甚至是另一个数组)。这意味着Array对其类型参数没有任何约束;对于任何Value,表达式Array<Value>都创建了一个完全有效且可用的类型。问题是,如果我有Array<A>Array<B>,它们可以互相赋值吗?
这就是协变性。
鉴于A和B之间的可分配性,方差指定了F和F之间的可分配性(其中F<...>是一个泛型)。
由于可分配性具有
方向性,因此有四种可能的情况:
  • 将类型包装在泛型中保持可分配性方向不变("协变性";泛型与其类型参数以相同的方向变化,即与其类型参数"协变");
  • 将类型包装在泛型中改变方向("逆变性";泛型与其类型参数以相反的方向变化,即与其类型参数"逆变");
  • 将类型包装在泛型中不允许任何方向变化("不变性");以及
  • 将类型包装在泛型中允许任何方向变化("双变性")。

将此与血型进行比较可能会有所帮助(尽管可能有点可怕):A型含有抗B抗体;B型含有抗A抗体;AB型不含抗体;最后,O型含有两种抗体。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接