我偶然发现了这种奇怪的行为,
如果我删除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();
但是withStringA
和withNumberA
都是用完全相同的构造调用new Foo()
初始化的。withStringA.a
怎么可能是string
类型而withNumberA.a
是number
类型?没有一个合理的机制可以让这样的事情发生;如果没有魔法,IFoo
是无法实现的,至少不安全。
特别地,class {}
没有正确地实现IFoo
。它根本没有属性,更不用说a
属性了,它神奇地是string
或number
,这取决于运行时不可用的信息。如果你尝试运行这个,你会得到一个运行时错误:
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中的一个设计限制;这是一个错误,但如果他们修复了它,许多当前工作的代码(碰巧是安全的(可能会开始发出编译器警告(因为编译器无法很好地验证过载安全性(。
因此,如果您决定为一个类型使用多个泛型调用或构造签名,请小心如何实现它,因为编译器可能无法自动发现错误。
到代码的游乐场链接