1

有一个问题我一直在努力解决。

我有一个函数,里面放了两个参数:

  • - 类数组
  • mapper - 采用这些类的实例数组的函数。

想象一下这样的功能:

const MapClasses = (classes, mapper) => {
  const instances = classes.map((item) => new item())
  return mapper(instances)
}

我如何为此创建一个打字稿声明,以确保映射器将始终接受实例的数组。

示例用法:

class Coords {
  x = 10
  y = 10
}

class Names {
  name = 'Some Name'
}

const mapper = ([coords, names]) => {
  return {
    x: coords.x,
    y: coords.y,
    myName: names.name,
  }
}

const mapped = MapClasses([Coords, Names], mapper)
// { x: 10, y: 10, myName: 'Some Name'}

所以我认为应该可以检查映射器是否访问了正确的值。

我的工作方式有点像:

type MapClasses = <Classes, Mapped>(
  classes: {
    [Property in keyof Classes]: new () => Classes[Property]
  },
  mapper: (instances: Classes) => Mapped,
) => Mapped

但是在这种情况下,错误仅显示在classes参数上,而不是mapper

那么有什么办法可以扭转这种行为呢?
……

我会感谢任何想法。

祝你今天过得愉快。

4

1 回答 1

1

需要明确的是,你的问题是当你调用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 规范中有一部分内容已经过时了。一个好的经验法则是编译器将以“最简单”的方式选择与类型参数相关的值。classesmapperclassesmapper

所以我们需要重构调用签名,使得类型注解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 代码链接

于 2021-12-20T14:29:40.807 回答