将类(而不是实例)传递给函数,这样静态方法仍然可以工作,而无需将类重新声明为接口



我想传递一个类(类本身,而不是它的实例)作为函数的参数,以便函数可以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链接到代码

最新更新