TypeScript 中未定义的协方差
Covariance with undefined in TypeScript
下面的代码显然是打错了,但我无法在上面打出TypeScript错误。我打开 strict
,以及 strictNullChecks
和 strictFunctionTypes
以取得良好效果,但 TS 仍然坚定地认为这段代码是好的和花花公子。
abstract class A {
// You can pass an undefined to any A
public abstract foo(_x: number | undefined);
}
class B extends A {
// B is an A, but it prohibits passing in an undefined.
// Note that if we did `x: string`, TS would flag it as
// an error.
public foo(x: number) {
if (x === undefined) {
throw new Error("Type error!");
}
}
}
function makeAnA(): A {
// This typechecks correct, so B is clearly an A, in
// TS's opinion.
return new B();
}
function test() {
const b = makeAnA();
// b is a B, so this should not be possible
b.foo(undefined);
}
这是预期的行为吗?是否有一些我可以打开的选项会将此标记为错误?我不止一次被这个咬过。
这是设计决定。所有方法参数的行为都是双变的。这意味着就 ts 而言,for methods (_x: number) => void
是 to (_x: number | number) => void
(和 vice-versa)的子类型。这显然是不合理的。
最初不仅方法参数的行为是双变的,而且所有函数签名参数也是。为了解决这个问题,在 typescript 2.6 中添加了 strictFuctionTypes
标志。来自 PR:
With this PR we introduce a --strictFunctionTypes mode in which function type parameter positions are checked contravariantly instead of bivariantly. The stricter checking applies to all function types, except those originating in method or constructor declarations. Methods are excluded specifically to ensure generic classes and interfaces (such as Array) continue to mostly relate covariantly. The impact of strictly checking methods would be a much bigger breaking change as a large number of generic types would become invariant (even so, we may continue to explore this stricter mode).
(highlight added)
所以在这里我们可以瞥见让方法参数继续双变相关的决定。是为了方便。如果没有这种不健全,大多数 类 将是不变的。例如,如果 Array
是不变的,Array<Dog>
就不会是 Array<Animal>
的子类型,在非常基本的代码中创建各种痛点。
虽然绝对不等价,但如果我们使用函数字段而不是方法(打开 strictFunctionTypes
),我们确实会得到一个错误 Type '(x: number) => void' is not assignable to type '(_x: number | undefined) => void'
abstract class A {
// You can pass an undefined to any A
public foo!: (_x: number | undefined) => void;
}
class B extends A {
// Error here
public foo: (x: number) => void = x => {
if (x === undefined) {
throw new Error("Type error!");
}
}
}
function makeAnA(): A {
//And here
return new B();
}
function test() {
const b = makeAnA();
// b is a B, so this should not be possible
b.foo(undefined);
}
注意:上面的代码仅在 strictFunctionTypes
时给出错误,因为没有它所有函数参数继续表现双变。
下面的代码显然是打错了,但我无法在上面打出TypeScript错误。我打开 strict
,以及 strictNullChecks
和 strictFunctionTypes
以取得良好效果,但 TS 仍然坚定地认为这段代码是好的和花花公子。
abstract class A {
// You can pass an undefined to any A
public abstract foo(_x: number | undefined);
}
class B extends A {
// B is an A, but it prohibits passing in an undefined.
// Note that if we did `x: string`, TS would flag it as
// an error.
public foo(x: number) {
if (x === undefined) {
throw new Error("Type error!");
}
}
}
function makeAnA(): A {
// This typechecks correct, so B is clearly an A, in
// TS's opinion.
return new B();
}
function test() {
const b = makeAnA();
// b is a B, so this should not be possible
b.foo(undefined);
}
这是预期的行为吗?是否有一些我可以打开的选项会将此标记为错误?我不止一次被这个咬过。
这是设计决定。所有方法参数的行为都是双变的。这意味着就 ts 而言,for methods (_x: number) => void
是 to (_x: number | number) => void
(和 vice-versa)的子类型。这显然是不合理的。
最初不仅方法参数的行为是双变的,而且所有函数签名参数也是。为了解决这个问题,在 typescript 2.6 中添加了 strictFuctionTypes
标志。来自 PR:
With this PR we introduce a --strictFunctionTypes mode in which function type parameter positions are checked contravariantly instead of bivariantly. The stricter checking applies to all function types, except those originating in method or constructor declarations. Methods are excluded specifically to ensure generic classes and interfaces (such as Array) continue to mostly relate covariantly. The impact of strictly checking methods would be a much bigger breaking change as a large number of generic types would become invariant (even so, we may continue to explore this stricter mode).
(highlight added)
所以在这里我们可以瞥见让方法参数继续双变相关的决定。是为了方便。如果没有这种不健全,大多数 类 将是不变的。例如,如果 Array
是不变的,Array<Dog>
就不会是 Array<Animal>
的子类型,在非常基本的代码中创建各种痛点。
虽然绝对不等价,但如果我们使用函数字段而不是方法(打开 strictFunctionTypes
),我们确实会得到一个错误 Type '(x: number) => void' is not assignable to type '(_x: number | undefined) => void'
abstract class A {
// You can pass an undefined to any A
public foo!: (_x: number | undefined) => void;
}
class B extends A {
// Error here
public foo: (x: number) => void = x => {
if (x === undefined) {
throw new Error("Type error!");
}
}
}
function makeAnA(): A {
//And here
return new B();
}
function test() {
const b = makeAnA();
// b is a B, so this should not be possible
b.foo(undefined);
}
注意:上面的代码仅在 strictFunctionTypes
时给出错误,因为没有它所有函数参数继续表现双变。