我试图理解为什么以下代码不起作用。
我有一个 mixin 可以生成一个匿名类。生成的类有一个带有定义的参数列表的构造函数。但是,当我尝试创建该类的实例时,TypeScript 返回错误:
Expected 0 arguments, but got 1
。
当然,我可以在TestCase类中定义一个构造函数,这样就解决了这个问题。但是,我想知道为什么这段代码不能按预期工作,以及是否有一种方法可以使其工作而无需向每个扩展类添加构造函数。
type Constructor<T = {}> = new (...args: any[]) => T;
function mixin<T extends Constructor>(Base: T) {
return class extends Base {
constructor(...args: any[]) {
super(...args);
}
};
}
@mixin
class TestCase {
}
new TestCase(1) // <- Expected 0 arguments, but got 1.
您不能使用装饰器来更改 TypeScript 中类的外观 type。目前对于随着 TypeScript 5.0 发布的新 ECMAScript 装饰器和旧的实验性 TypeScript 装饰器都是如此。
这意味着如果你的装饰器是一个 mixin 并且它添加了一个属性或更改了构造函数参数,则该更改对于 TypeScript 将不可见(即使它当然会在运行时发生):
function mixin<C extends new (...args: any) => any>(Base: C) {
return class extends Base {
addedProp = "abc"; // adds a property
constructor(...args: any) {
super(...args);
}
};
}
@mixin class TestCase {
constructor(public prop: number) { }
}
const testCase = new TestCase(1); // okay
console.log(testCase.prop.toFixed(2)); // "1.00"
console.log(testCase.addedProp.toString()); // "ABC" error!
// ~~~~~~~~~
// Property 'addedProp' does not exist on type 'TestCase'.
所以你不应该使用装饰器作为 mixin,除非 mixin 不应该改变类构造函数的类型。这是装饰器作为混合的限制。microsoft/TypeScript#4881有一个长期开放的功能请求,以支持改变类的类型,但从 TypeScript 5.6 开始尚未实现。
您仍然可以将其用作常规函数调用,而不是使用 mixin 作为装饰器:
const TestCase = mixin(
class TestCase {
constructor(public prop: number) { }
}
);
const testCase = new TestCase(1); // okay
console.log(testCase.prop.toFixed(2)) // "1.00"
console.log(testCase.addedProp.toUpperCase()); // "ABC"
不幸的是,mixins的另一个问题是你无法轻松地编写它们来更改构造函数参数。 microsoft/TypeScript#37143 有一个开放功能请求以允许此类操作,但尚未实现。
当
new TestCase(1)
具有零参数构造函数时,您希望 class TestCase {}
成功的唯一原因可能是因为 mixin 在添加的属性中使用该 1
。 (我已经问过好几次了,如果不需要这样的参数,为什么人们想要将 1
作为参数传递给超级构造函数,而且我还没有听到令人信服的答复。所以我假设 mixin 使用第一个参数并传递其余的参数)。
但是由于缺少 microsoft/TypeScript#37143,明显的实现给出了错误:
function mixin<C extends new (...args: any) => any>(Base: C) {
return class extends Base { // error!
// ~~~~~ error! A mixin class must have a constructor
// with a single rest parameter of type 'any[]'.(2545)
constructor(public addedProp: string, ...args: ConstructorParameters<C>) {
super(...args);
}
}
}
因此,如果您希望这种情况发生,您需要对类型进行更直接的控制。您必须明确地写出您期望 mixin 执行的操作,并使用大量类型断言
来消除编译器错误:
function mixin<A extends any[], T extends object>(Base: new (...args: A) => T) {
return class extends (Base as any) {
constructor(public addedProp: string, ...args: A) {
super(...args);
}
} as new (addedProp: string, ...args: A) => (T & { addedProp: string })
}
现在,当您调用它时,返回的构造函数类型会将新参数添加到超级构造函数所需的列表中,并返回带有添加属性的实例类型:
const TestCase = mixin(
class TestCase {
constructor(public prop: number) { }
}
);
/* const TestCase: new (addedProp: string, prop: number) => TestCase & {
addedProp: string;
} */
const testCase = new TestCase("abc", 1);
console.log(testCase.prop.toFixed(2)) // "1.00"
console.log(testCase.addedProp.toUpperCase()) // "ABC"