【问题标题】:Typescript: converting type of array of classes to type of array of constructed classes打字稿:将类数组的类型转换为构造类数组的类型
【发布时间】:2022-01-21 17:50:51
【问题描述】:

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

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

  • - 类数组
  • ma​​pper - 采用这些类的实例数组的函数。

想象一下这样的功能:

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

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

示例用法:

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 参数上,而不是 ma​​pper

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

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

祝你有美好的一天。

【问题讨论】:

  • 我很难理解你想要做什么,但我怀疑你将能够使用像 Array.prototype.map 这样的内置插件来完成你想要做的事情。如果您看起来像是在尝试对原始类而不是实例进行操作,那么 TS 会引发很多障碍。您能否详细说明您的解决方案的用途?
  • this approach 是您要找的吗?如果您的问题包含您要解决的实际问题的minimal reproducible example,这将很有帮助(即错误出现在classes 而不是mapper);你能edit那个吗?如果我的解决方案适合你,我可以写一个答案;如果没有,请告诉我我缺少什么。
  • @jcalz 正是我想要的。

标签: javascript typescript typescript-typings typescript-generics typescript-class


【解决方案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 的签名,以便从classes 而非mapper 推断generic 类型参数。这意味着我们需要classesmapper 具有更高的类型参数优先级推理站点。编译器如何选择从哪些值中推断类型的细节并没有真正记录在任何官方的地方;不过,有一个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它是一个construct signatures的数组。从概念上讲,classes 只是C 类型,尽管我已经使用variadic tuple type 编写了[...C],以向编译器提示我们希望将classes 推断为元组而不是无序数组。

同时mapper(instance: ElementInstanceType&lt;C&gt;) =&gt; M 类型,其中ElementInstanceTypemapped type,其属性为conditional types。它将构造签名类型的元组转换为它们对应的实例类型的元组。比较[...C](instance: ElementInstanceType&lt;C&gt;) =&gt; 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 link to code

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2022-10-14
    • 1970-01-01
    • 2017-01-29
    • 2019-07-05
    • 2019-03-22
    • 2020-08-15
    • 2021-02-14
    相关资源
    最近更新 更多