我想传递一个类(类本身,而不是它的实例)作为函数的参数,以便函数可以1)调用该类的静态方法和2)实例化该类。
我已经让它工作了,但是我唯一能做的就是将整个类结构重新声明为接口,所以我有效地声明了两次类-一次在class {}
声明中,另一次作为函数的参数类型。
是否有一种方法可以避免这样做,以便我可以重用类定义而没有重复的类型?
下面是演示这个问题的工作代码:class Base {
static id() { return { name: 'example', code: 'none' }; }
static other1() { return 'example1'; }
static other2() { return 'example2'; }
constructor(a: number, b: number) { }
}
class A extends Base {
static id() { return { ...super.id(), code: 'class-a' }; }
static other1() { return 'example1A'; }
constructor(a: number, b: number) { super(a, b + 1); }
}
class B extends Base {
static id() { return { ...super.id(), code: 'class-b' }; }
static other2() { return 'example2B'; }
constructor(a: number, b: number) { super(a, b + 2); }
}
// I have to re-declare all the static functions here. How can I replace
// this type with one that does it automatically from class `Base`?
interface BaseClassAsParameter<T> {
new(...args: any): T;
id: () => any;
other1: () => any;
other2: () => any;
// If I add another static function to Base, I have to add it here too.
}
function testClass<T extends Base>(C: BaseClassAsParameter<T>): T {
const { name, code } = C.id();
const other1 = C.other1();
const other2 = C.other2();
console.log(`Test ${name} code ${code}: ${other1} ${other2}`);
const x = new C(100, 200);
// do something with x
return x;
}
testClass(A); // should print "Test example code class-a: example1A example2"
testClass(B); // should print "Test example code class-b: example1 example2B"
这样做的一种方法是使您的函数在C
参数类型中具有泛型,限制为typeof Base
:
function testClass<T extends typeof Base>(C: T) {
const { name, code } = C.id();
const other1 = C.other1();
const other2 = C.other2();
console.log(`Test ${name} code ${code}: ${other1} ${other2}`);
const x = new C(100, 200) as InstanceType<T>;
return x;
}
testClass(A); // A
testClass(B); // B
这可以工作,因为值Base
具有您关心的所有静态方法,并且还有一个构造签名,该签名接受两个数字并产生Base
的实例。这将最终要求您传递给testClass()
的任何内容也具有相同的功能。注意,并不是所有Base
的子类都是这样工作的:
class Nope extends Base {
constructor(x: string) { super(x.length, 2) }
}
testClass(Nope); // error!
// -----> ~~~~
// Type 'number' is not assignable to type 'string'.
失败,因为值Nope
在其构造签名中不接受两个数字。
不管怎样,这是可行的。唯一可能的问题是编译器过早地将new C(100, 200)
的类型扩展为特定的Base
类型,而不是保持它的泛型。正确的类型是InstanceType<T>
,但这是一个条件类型,编译器无法验证一个值是否可赋值给泛型条件类型。这是TypeScript当前的一个限制。(有一堆相关的GitHub问题;例如,microsoft/TypeScript#44049是关于ReturnType<T>
的类似问题。)
很容易通过new C(100, 200) as InstanceType<T>
使用上面的类型断言,然后继续。这可以防止编译器错误,但只是因为您告诉编译器您知道new C(100, 200)
确实是InstanceType<T>
类型。编译器不会验证这一点,因此如果你碰巧错了,也不会抓住你。
如果你想从编译器获得更多的类型安全,你可以继续从Base
定义BaseClassAsParameter<T>
实用程序类型。这样的:
type BaseClassAsParameter<T extends Base> =
(new (...args: ConstructorParameters<typeof Base>) => T) &
typeof Base
这里我们手动提供了一个更具体的构造签名,其中BaseClassAsParameter<T>
的实例类型是T
,但仍然使用ConstructorParameters<T>
实用程序类型从Base
提取构造函数参数。通过将呼叫签名与typeof Base
相交,我创建了一个技术上具有两个呼叫签名的类型;第一个是返回T
实例的特定实例,第二个是返回Base
实例的原始实例。当我们调用构造签名时,编译器将使用与重载相同的规则,并使用第一个匹配的签名进行解析…因此,通过将特定的一个放在前面,我们应该得到那个。
让我们测试一下:
function testClass<T extends Base>(C: BaseClassAsParameter<T>) {
const { name, code } = C.id();
const other1 = C.other1();
const other2 = C.other2();
console.log(`Test ${name} code ${code}: ${other1} ${other2}`);
const x = new C(100, 200);
return x;
}
testClass(A); // A
testClass(B); // B
看起来不错。现在编译器自动理解new C(100, 200)
产生了一个T
类型的值,并且仍然知道C
上的静态方法。
Playground链接到代码