【问题标题】:Mapping tuple-typed value to different tuple-typed value without casts将元组类型的值映射到不同的元组类型的值而不进行强制转换
【发布时间】:2021-05-11 10:31:50
【问题描述】:

我有一个元组类型的值,我想将它映射到不同类型的另一个元组。允许元组是异质的。有没有办法在不使用下面示例中的类型转换的情况下执行此映射?

interface SomeType<T> {
  value: T
}

type SomeTypeList<T extends any[]> = { [K in keyof T]: SomeType<T[K]> }

interface OtherType<T> {
  otherValue: T
}

type OtherTypeList<T extends any[]> = { [K in keyof T]: OtherType<T[K]> }

function convertValues<T extends any[]> (arr: SomeTypeList<T>): OtherTypeList<T> {
  // Is there any way to write this without the cast?
  return arr.map(({ value }) => ({ otherValue: value })) as OtherTypeList<T>
}

convertValues<[number, string, boolean]>([{ value: 1 }, { value: 'cat' }, { value: true }])
// => [{ otherValue: 1 }, { otherValue: 'cat' }, { otherValue: true }]

使用强制转换会失去类型安全性,因为回调可能会返回 ({ otherValue: 1 }) 并且所有内容都会正确地进行类型检查。

【问题讨论】:

    标签: typescript typescript-typings typescript-generics


    【解决方案1】:

    不,从 TS4.1 开始,这在 TypeScript 中是不可能的。我看到了两个问题;一个可能会通过更改 Array.prototype.map() 的 TypeScript 标准库类型来克服,但另一个需要对类型系统进行相当大的更改才能正常工作,并且目前无法以某种方式处理,在某些情况下感觉,相当于type assertion(你称之为“演员”)。


    数组的map() 方法的current library typings 是:

    interface Array<T> {
      map<U>(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any): U[];
    }
    

    返回类型U[] 是一个无序数组类型,对应于callbackfn 参数的输出类型。它不是tuple。有一个开放的 GitHub 问题,microsoft/TypeScript#29841 要求更改它,以便当您在元组上调用 map() 时,您会得到一个相同长度的元组。目前尚未实施;但是您可以使用declaration merging 自己测试这样的更改:

    interface Array<T> {
      map<This extends Array<T>, U>(this: This, fn: (v: T) => U): { [K in keyof This]: U }
    }
    

    如果我调用convertValues()(将arr 参数更改为variadic tuple type,这给编译器一个提示,如果可能的话,它应该将arr 解释为一个元组),你现在可以看到它是如何实现的知道返回值中有多少个元素:

    function convertValues<T extends any[]>(arr: [...SomeTypeList<T>]) {
      return arr.map(({ value }) => ({ otherValue: value }))
    }
    
    const ret = convertValues([{ value: 1 }, { value: 'cat' }, { value: true }])
    ret[0]; ret[1]; ret[2]; // okay
    ret[3] // error! Tuple type of length '3' has no element at index '3'.
    

    不幸的是,每个元组元素的类型是未知的:

    ret[0].otherValue.toFixed(2); // error!
    // -------------> ~~~~~~~
    // Property 'toFixed' does not exist on type 'string | number | boolean'.
    

    每个元素只有编译器知道是{otherValue: string | number | boolean}。这并没有错,但也不是您想要的。


    所以,让我们退后一步,想想你需要map() 成为什么样的人,才能做你想做的事。当您调用arr.map() 时,您会将回调视为generic 函数,它将泛型类型SomeType&lt;V&gt; 的输入转换为OtherType&lt;V&gt; 类型的输出,对于任何V。否则没有机会让编译器注意到输入元组的每个元素与输出元组的对应元素之间的相关性。您确实可以在调用 map() 时通过注释回调来编写:

    function convertValues<T extends any[]>(arr: [...SomeTypeList<T>]) {
      return arr.map(<V,>(
        { value }: SomeType<V>
      ): OtherType<V> => ({ otherValue: value }))
    }
    

    问题是......你如何描述这种通用回调一般可能会做其他事情?我假设您不想对map() 的输入进行硬编码,以关心专门将SomeType&lt;V&gt; 转换为OtherType&lt;V&gt; 的回调。您想说“对于任何 FG,将 F&lt;X&gt; 转换为 G&lt;X&gt; 的回调”:

    // not valid TypeScript, don't use it
    interface Array<T> {
      map<A extends any[], F<?>, G<?>>(
        this: { [K in keyof A]: F<A[K]>},
        fn: <V>(v: F<V>) => G<V>
      ): { [K in keyof A]: G<A[K]> }
    }
    

    但是没有办法在 TypeScript 中表达这一点。上面的FG 不是泛型类型,而是泛型类型函数类型构造函数。而这些在 TypeScript 中不存在。泛型类型构造函数需要引入所谓的higher kinded types,这可以在一些更注重函数式编程的语言(如 Haskell 和 Scala)中找到。有一个长期开放的功能请求,microsoft/TypeScript#1213 要求这样做,但谁知道它是否会实现。这将是相当多的工作,所以我没有屏住呼吸(但很想看到它!)。并且有一些可能的方法可以在 TypeScript 中模拟更高种类的类型(您可以阅读 GitHub 问题了解更多信息),但我不想为您的用例推荐任何方法。

    所以我们陷入了困境。目前没有办法编写map() 的类型来让编译器验证convertValues() 的实现是否符合您声称返回的类型。


    当编译器无法验证某事物的类型是否就是您声称的那样,并且您确信您的声明仍然正确时,您几乎需要执行类型断言之类的操作,因为您已经完成了。因此,我建议您继续按照此处显示的方式进行操作,如果 TypeScript 中出现过更高种类的类型,请重新访问。

    Playground link to code

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2022-01-23
      • 1970-01-01
      • 2020-01-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-06-04
      相关资源
      最近更新 更多