TypeScript 中未定义的协方差

Covariance with undefined in TypeScript

下面的代码显然是打错了,但我无法在上面打出TypeScript错误。我打开 strict,以及 strictNullChecksstrictFunctionTypes 以取得良好效果,但 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);
}

Playground Link

注意:上面的代码仅在 strictFunctionTypes 时给出错误,因为没有它所有函数参数继续表现双变。