需要明确的是,你的问题是当你调用MapClasses()
不正确时,错误出现在classes
参数而不是mapper
参数上:
MapClasses([Names, Coords], mapper) // error
// -------> ~~~~~ ~~~~~~ <---
// | |
// | Type 'typeof Coords' is not assignable to type 'new () => Names'.
// Type 'typeof Names' is not assignable to type 'new () => Coords',
这没有错,但您希望它以另一种方式发生,如下所示:
MapClasses([Names, Coords], mapper) // error
// -----------------------> ~~~~~~
// Type '[Names, Coords]' is not assignable to type '[Coords, Names]'.
为了实现这一点,我们需要更改 的签名,MapClasses
以便从而不是从推断泛型类型参数。这意味着我们需要比类型参数具有更高优先级的推理站点。编译器如何选择从哪些值来推断类型的细节并没有真正记录在任何官方的地方;不过,过时的 TypeScript 规范中有一部分内容已经过时了。一个好的经验法则是编译器将以“最简单”的方式选择与类型参数相关的值。classes
mapper
classes
mapper
所以我们需要重构调用签名,使得类型注解classes
与类型参数的关系比类型注解更简单mapper
。这是一种方法:
const MapClasses = <C extends (new () => any)[], M>(
classes: [...C],
mapper: (instances: ElementInstanceType<C>) => M,
): M => {
const instances = classes.map((item) => new item()) as
ElementInstanceType<C>;
return mapper(instances)
}
type ElementInstanceType<C> =
{ [K in keyof C]: C[K] extends new () => infer R ? R : never };
类型参数C
是我们所关心的,我将它限制为一个构造签名数组。概念classes
上只是 type C
,尽管我已经[...C]
使用可变元组类型来给编译器一个提示,我们希望将其推断classes
为元组而不是无序数组。
同时mapper
是类型(instance: ElementInstanceType<C>) => M
,其中ElementInstanceType
是一个映射类型,其属性是条件类型。它将构造签名类型的元组转换为它们对应的实例类型的元组。对比[...C]
一下(instance: ElementInstanceType<C>) => M
,你会发现前者C
比后者更简单。
这意味着当您调用时,MapClasses
类型参数C
将倾向于从中推断classes
并仅检查。mapper
让我们确保它有效。首先,我们需要看到非错误情况仍然会产生正确类型的值:
const mapped = MapClasses([Coords, Names], mapper) // okay
// const mapped: { x: number; y: number; myName: string; }
现在我们应该看看出现错误时会发生什么:
MapClasses([Names, Coords], mapper) // error
// -----------------------> ~~~~~~
// Type '[Names, Coords]' is not assignable to type '[Coords, Names]'.
是的,这就是我们想要看到的。
Playground 代码链接