如何采用任何构造函数类型并将其转换为接受相同参数的函数类型?

how to take any constructor type and convert it to a function type which accepts the same arguments?

考虑下面的代码,我创建了一个class,它有一个构造函数,它接收一个泛型类型的参数,用于定义第二个参数接收的参数类型,在这种情况下回调函数的a参数下面是MouseEvent,因为第一个参数的值:

class MyClass<T extends keyof HTMLElementEventMap>{
    constructor(tagName: T, lister: (ev: HTMLElementEventMap[T]) => any) { }
}

new MyClass("click", ev => { })
//                   ^^ (parameter) ev: MouseEvent

但是,如果我创建一个函数,其剩余参数类型为 ConstructorParameters,则第二个参数改为解析回调参数的 MouseEvent 类型,已解析 Event | UIEvent | AnimationEvent | MouseEvent | FocusEvent | DragEvent | ErrorEvent | PointerEvent | ... 6 more ... | ClipboardEvent

function myFunction(...args: ConstructorParameters<typeof MyClass>) {}

myFunction("click", ev => { })
//                  ^^ (parameter) ev: Event | UIEvent | AnimationEvent | MouseEvent | FocusEvent | DragEvent | ErrorEvent | PointerEvent | ... 6 more ... | ClipboardEvent

如何在不重写 myFunction 函数的类型的情况下获得回调参数的正确类型?

可能吗?

你想要的是“采用任何构造函数类型并将其转换为接受相同参数的 function 类型”。


一个问题是 TypeScript 的类型系统没有很好的方式来表达适用于 generic 函数的“可调用或可构造事物的参数”。如果你有一个像

这样的函数
function foo<T>(x: T, y: T): void { }

并尝试获取它的参数列表,泛型参数T将被替换为它的constraint。在这种情况下,T 不受约束,因此具有 unknown:

的隐式约束
type FooParams = Parameters<typeof foo>;
// type FooParams = [x: unknown, y: unknown]

TypeScript 没有正确的泛型类型来表示泛型函数的参数列表。泛型函数在调用签名上有其泛型类型参数。但是像 [x: unknown, y: unknown] 这样的元组类型没有调用签名并且不能采用泛型类型参数:

// Not valid TS, do not use this:
type FooParams = <T>[x: T, y: T];

为了表示这一点,TypeScript 需要像 通用值类型 之类的东西,如 microsoft/TypeScript#17574 中所要求的...但它没有这些。


好吧,不用担心元组类型,也许我们可以自动将一种通用函数类型转换为另一种。再次不幸的是,该语言没有合适的类型运算符来执行此操作。为了捕获通用函数与其类型参数之间的关系,TypeScript 可能需要像 microsoft/TypeScript#1213 中要求的“高等类型”之类的东西......但它也没有这些。


在 TypeScript 3.4 之前,我想我们会完全陷入困境。但是 Typescript 3.4 引入了对 higher order type inference from generic functions 的支持。这是更高种类类型的部分实现,但没有可用的类型级语法来使用它。可以将一个实际的泛型函数value转化为另一个泛型函数value,其中输出函数的类型与输入函数的类型相关完全如你所愿。如果您没有实际的函数值,那么您可以制作一个或假装制作一个,对该值进行高阶推理,然后获取输出函数的类型。但是,正如您所指出的,如果您只关心输入,那么这会向您的输出添加一些您根本不需要的实际 JavaScript 。对于 MyClass 示例,它看起来像这样:

// abuse TS3.4 support for higher order inference from generic functions
const ctorArgs = <A extends any[], R>(f: new (...a: A) => R): (...a: A) => void => null!
const myFunc = ctorArgs(MyClass)
type MyFunctionType = typeof myFunc;

const myFunction: MyFunctionType = (...args) => {}
myFunction("click", ev => { ev.buttons })

您可以看到 ctorArgs 正在执行您想要的类型操作,但在值级别。在上面的代码中,ctorArgs 丢弃了它的输入,只是假装 return 一个函数;并且我们依次丢弃输出并获取其类型。

由于您无法逃避其中的 JavaScript 部分,也许您实际上可以利用它发挥您的优势并通用地实施 myFunction。你会做什么,需要一个构造函数并将它变成一个 void-returning 函数?调用 new 然后丢弃结果?像这样:

const makeMyFunction = <A extends any[], R>(
  f: new (...a: A) => R): (...a: A) => void =>
    (...a) => { new f(...a) } // whatever the implementation should do

const myFunction = makeMyFunction(MyClass);

这样的话,myFunction就免费出来了。但是这是否适用取决于你的用例,我不知道。


Playground link to code