TL;DR
export type ConstructorType<C extends abstract new (...args: any) => any> = (
...args: ConstructorParameters<C>
) => InstanceType<C>;
typeof?
由于 JavaScript 的 class 无法很好 tree shaking,加上函数写起来非常方便,所以平时很少用 class。但最近开始折腾 langchainjs,它基本上是跟 python 版本一一对应的,所以写得很向对象也大量用了 class。我想写一个工厂函数传入一些默认参数,却发现拿不到准确的 constructor 的类型。在 tg 群友的指点下搞懂了这里问题,关键就在于区分实例类型和 constructor 的类型。
import { OpenAI } from "langchain/llms/openai";
const createOpenAI: OpenAI["constructor"] = (options, config) => {
// ^? Function
return new OpenAI({ temperature: 0, ...options }, config);
};
这个问题可以简化成下面的代码:
class Foo {
constructor(foo: string) {}
}
type A = Foo;
type B = typeof Foo;
type C = A["constructor"];
// ^? type C = Function
如何从Foo
通过类型运算获取到(name: string) => Foo
这样的类型,而不是像上面一样拿到的是个Function
。
在 TypeScript 中typeof
除了运行态的语义外,还可以将 Value 转成 Type 用做类型运算。例如const a: typeof 123 = 456
。但上面这个A
跟B
通过 alias 拿到的都是类型却很不一样。
A 是实例化后的 instanceType,而 B 是构造器 constructor 的 type
在运行态 JavaScript 中实例原型链上的 constructor 字段引用指向Foo
,所以A['constructor']
会提示Function
类型(没有精确的参数类型)。这个 GitHub issue 在讨论这个行为。
const a = new Foo("a");
a.constructor === Foo; // true
再来看下面这个例子:
class Foo {
constructor(public bar: string) {}
static baz: 123;
}
type A = Foo;
type B = typeof Foo;
type C = A["bar"]; // string
type D = B["bar"]; // Property 'bar' does not exist on type 'typeof Foo'.(2339)
type E = A["baz"]; // Property 'baz' does not exist on type 'Foo'.(2339)
type F = B["baz"]; // 123
Foo 中声明了两个 field,其中的 baz 是个 static filed,A 由于是实例化过的所以有 bar 没 baz,而 B 由于是 constructor 所以有 baz,没 bar
infer 实现
实际上 TypeScript 文档 中有提到constructor signature,构造函数类型可以写成new (...args: P) => T
TL;DR 中的实现可以不借助 内置的 Utility Types 而用 infer 实现:Playground。
type ConstructorType<C> = C extends abstract new (...args: infer P) => infer R
? (...args: P) => R
: never;