如果删除了重复的构造函数,则泛型类表达式已损坏



我偶然发现了这种奇怪的行为,

如果我删除IFoo中的一个构造函数签名,编译器将触发以下错误:Type 'typeof (Anonymous class)' is not assignable to type 'IFoo'.

实际发生了什么?

type Foo<T> = {
[S in keyof T]: T[S]
}
interface IFoo {
new <T>(): Foo<T>;
new <T>(): Foo<T>; // KO if removed
}
const Foo: IFoo = (class {})
const foo = new Foo();

游乐场

TL;DR:如microsoft/TypeScript#26631和microsoft/TypeScript#50050中所述,未正确检查通用重载的类型。


您的IFoo接口声称是一个泛型类构造函数的类型,该构造函数接受一个类型参数T,但没有实际值参数,并返回一个类型为Foo<T>的类实例,该类实例与T大致相同(因为它是复制所有非签名属性的标识映射类型(。所以我有一个类型为IFoo的值Foo,那么我大概可以写这个:

declare const Foo: IFoo;
const withStringA = new Foo<{a: string}>();
withStringA.a.toUpperCase();
// const withStringA: Foo<{ a: string; }>
const withNumberA = new Foo<{a: number}>();
// const withNumberA: Foo<{ a: number; }> 
withNumberA.a.toFixed();

其将作为以下JavaScript发出:

const withStringA = new Foo();
withStringA.a.toUpperCase();
const withNumberA = new Foo();
withNumberA.a.toFixed();

但是withStringAwithNumberA都是用完全相同的构造调用new Foo()初始化的。withStringA.a怎么可能是string类型而withNumberA.anumber类型?没有一个合理的机制可以让这样的事情发生;如果没有魔法,IFoo无法实现的,至少不安全。

特别地,class {}没有正确地实现IFoo。它根本没有属性,更不用说a属性了,它神奇地是stringnumber,这取决于运行时不可用的信息。如果你尝试运行这个,你会得到一个运行时错误:

const Foo: IFoo = class { };
const withStringA = new Foo<{ a: string }>();
// const withStringA: Foo<{ a: string; }>
withStringA.a.toUpperCase(); // RUNTIME ERROR 💥 x.a is undefined 

那么,如果IFoo不能安全地实现,为什么编译器不警告您?


事实上,正如您所注意到的,当您删除第二个构造签名:时,您会收到警告

interface IFoo {
new <T>(): Foo<T>;
}
const Foo: IFoo = class { }; // error!
// -> ~~~
// Type 'typeof Foo' is not assignable to type 'IFoo'.

此错误是预期的良好

但是,一旦添加第二个重载的构造签名,错误就会消失:

interface IFoo {
new <T>(): Foo<T>;
new <T>(): Foo<T>; 
}
const Foo: IFoo = class { }; // okay?!

为什么?


问题是未正确检查通用重载的类型,如microsoft/TypeScript#26631和microsoft/TypeScript#50050中所述。类型参数被替换为不安全的any类型,这意味着编译器检查class {}的赋值,就好像IFoo是:

interface IFooLike {
new(): Foo<any>;
new(): Foo<any>;
}

与相同

interface IFooLike {
new(): any;
new(): any;
}

事实上,class {}确实符合这种类型:

const Foo: IFooLike = class { };

所以没有编译器错误,即使应该有。

这实际上是TypeScript中的一个设计限制;这是一个错误,但如果他们修复了它,许多当前工作的代码(碰巧是安全的(可能会开始发出编译器警告(因为编译器无法很好地验证过载安全性(。

因此,如果您决定为一个类型使用多个泛型调用或构造签名,请小心如何实现它,因为编译器可能无法自动发现错误。


到代码的游乐场链接

最新更新