打字稿:将 类 的数组类型转换为构造的 类 数组类型

Typescript: converting type of array of classes to type of array of constructed classes

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

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

想象一下这样的函数:

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

我如何为此创建打字稿声明,以确保 mapper 将始终接受 类[=47 的实例数组=].

用法示例:

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

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

那么有什么方法可以扭转这种行为吗?
....

如有任何想法,我将不胜感激。

祝你有愉快的一天。

要清楚,你的问题是当你错误地调用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 的签名,以便 generic type parameter is inferred from classes and not from mapper. This means we need classes to have a higher priority inference site for the type parameter than mapper does. The details of how the compiler chooses which values to infer types from are not really documented anywhere official; there is a section of the outdated TypeScript spec 在某个时候超越过去的状态。一个好的经验法则是编译器将以“最简单”的方式选择与类型参数相关的值。

所以我们需要重构调用签名,使 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 是我们关心的参数,我有 constrained it to be an array of construct signatures. Conceptually classes is just of type C, although I have written [...C] using a variadic tuple type 给编译器一个提示,我们想将 classes 推断为一个元组和不是无序数组。

同时 mapper(instance: ElementInstanceType<C>) => M 类型,其中 ElementInstanceTypemapped type whose properties are conditional types。它将构造签名类型的元组转换为其对应实例类型的元组。比较 [...C](instance: ElementInstanceType<C>) => M,你会发现前者与 C 的关系比后者更简单。

这意味着当您调用 MapClasses 时,类型参数 C 将倾向于从 classes 中推断出来,而只是 checked 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 link to code