TypeScript 中方差、协方差、逆变和双方差的区别

Difference between Variance, Covaraince, Contravariance and Bivariance in TypeScript

能否使用简单的 TypeScript 示例解释什么是方差、协方差、逆变和双方差?

[持续更新]

有用链接:

  1. Another one good answer of Oleg Valter与主题相关

  2. Very good explanation of*-riance by Titian-Cernicova-Dragomir

  3. Stephan Boyer 博客

  4. Scala documentation - 很好的解释和例子

  5. @提香的

  6. @提香的

  7. Vlad Riscutia 's blog

  8. Mark Seemann 's文章

  9. @jcalz

Variance 与泛型类型 F<T> varies 相对于其类型参数 T.如果你知道 T extends U,那么方差会告诉你是否可以得出 F<T> extends F<U>,得出 F<U> extends F<T>,或者两者都不能得出。


Covariance 表示 F<T>T co-vary.也就是说,F<T> 变化(与 T 的方向相同)。换句话说,如果T extends U,那么F<T> extends F<U>。示例:

  • 函数或方法类型与其 return 类型共同变化:

    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!
    }
    

其他(暂时未显示)示例是:

  • 对象在它们的 属性 类型中是协变的,即使这听起来不适合可变属性
  • class 构造函数在它们的实例类型中是协变的

Contravariance表示F<T>Tcontra-vary.也就是说,F<T> 相反(与 T 的方向相反)。换句话说,如果T extends U,那么F<U> extends F<T>。示例:

  • 函数类型与其参数类型相反(启用 --strictFunctionTypes):

    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
    }
    

其他(暂时未显示)示例是:

  • 对象的键类型是逆变的
  • class 构造函数在它们的构造参数类型中是逆变的

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

  • return与其参数类型相同的函数类型在该类型中既不共变也不对变:

    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> 变化 both with and against TF<T>T中既是协变的又是逆变的。在健全的类型系统中,对于任何非平凡的类型函数,这基本上 永远不会发生 。您可以证明只有像 type F<T> = string 这样的常量类型函数才是真正的双变函数(速写:T extends unknown 对所有 T 都成立,所以 F<T> extends F<unknown>F<unknown> extends T,在声音类型系统中,如果 A extends BB extends B,则 AB 相同。所以如果 F<T> = F<unknown> 对所有T,则F<T>为常数)。

但是 Typescript 没有 nor does it intend to have a fully sound type system. And there is one notable case TypeScript 将类型函数视为双变:

  • 方法类型与其参数类型共同变化和反向变化(这也发生在禁用 --strictFunctionTypes 的所有函数类型中):

    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
    }
    

Playground link to code