正如您所指出的,数组协方差不可靠,可能会导致运行时出错。 TypeScript's Design Non-Goals 之一是
- 应用健全或“可证明正确”类型的系统。相反,应在正确性和生产力之间取得平衡。
这意味着,如果某些不健全的语言功能非常有用,并且如果要求健全性会使该语言使用起来非常困难或令人讨厌,那么尽管存在潜在的陷阱,但它很可能会留下来。
显然,"a fool's errand" 试图保证以描述 JavaScript 为主要目的的语言的健全性。
我想说的是,这里的根本问题是 TypeScript 想要支持一些非常有用的功能,不幸的是,这些功能不能很好地配合使用。
第一个是subtyping,其中类型形成层次结构,单个值可以是多种类型。如果S 类型是T 类型的子类型,那么S 类型的值s也是T 类型的值。例如,如果您有一个string 类型的值,那么您也可以将它用作string | number 类型的值(因为对于任何X,string 是string | X 的子类型)。 TypeScript 中接口和类层次结构的整个大厦都是建立在子类型的概念之上的。当S extends T 或S implements T 时,表示S 是T 的子类型。如果没有子类型,TypeScript 会更难使用。
第二个是aliasing,您可以使用多个名称引用相同的数据,而不必复制它。 JavaScript 允许这样做:const a = {x: ""}; const b = a; b.x = 1;。除了原始数据类型,JavaScript 值都是引用。如果您尝试在不传递引用的情况下编写 JavaScript,那将是一种非常不同的语言。如果 TypeScript 强制要求为了将对象从一个命名变量传递到另一个变量,您必须复制其所有数据,那么使用起来会更加困难。
第三个是mutability。 JavaScript 中的变量和对象通常是可变的;您可以重新分配变量和对象属性。不可变语言更容易推理/更简洁/更优雅,但对事物进行变异很有用。 JavaScript 不是不可变的,因此 TypeScript 允许它。如果我有一个值const a: {x: string} = {x: "a"};,我可以跟进a.x = "b"; 而不会出错。如果 TypeScript 要求所有别名都是不可变的,那就更难使用了。
但是将这些功能放在一起,事情可能会变糟:
let a: { x: string } = { x: "" }; // subtype
let b: { x: string | number }; // supertype
b = a; // aliasing
b.x = 1; // mutation
a.x.toUpperCase(); // ?? explosion
Playground link to code
一些语言通过要求variance 标记来解决这个问题。 Java 的 wildcards 用于此目的,但它们的正确使用相当复杂,而且(据说)被认为令人讨厌和困难。
TypeScript 决定在这里不做任何事情,并将所有属性类型视为协变,尽管 suggestions to the contrary。在这方面,生产力比正确性更重要。
出于类似的原因,在 TypeScript 2.6 引入 --strictFunctionTypes 编译器选项之前,函数和方法参数被检查 bivariantly,此时仍然始终只检查方法参数。
双变量类型检查是不健全的。但它很有用,因为它允许突变、别名和子类型化(不会因为要求开发人员跳过障碍而损害生产力)。 TypeScript 中的方法参数双方差导致数组协方差。
好的,希望对您有所帮助;祝你好运!