typescript 将元组类型的值Map到不同的元组类型的值而不进行强制转换

vhmi4jdf  于 2022-11-18  发布在  TypeScript
关注(0)|答案(1)|浏览(152)

我有一个元组类型的值,我想把它Map到不同类型的元组。元组允许是异质的。有没有一种方法可以在不使用下面例子中的类型转换的情况下执行这种Map?

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 }),并且所有内容都将正确进行类型检查。

pdsfdshx

pdsfdshx1#

不,这在TS4.1的TypeScript中是不可能的。其中一个问题可以通过更改Array.prototype.map()的TypeScript标准库类型来解决,但另一个问题需要对类型系统进行相当大的更改才能正常工作,并且当前无法以某种意义上不等效于类型Assert的方式进行处理(您称之为“强制转换”)。
数组的map()方法的当前库类型为:

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

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

// declare global { // uncomment if in a module
interface Array<T> {
  map<This extends Array<T>, U>(this: This, fn: (v: T) => U): { [K in keyof This]: U }
}
interface ReadonlyArray<T> {
  map<This extends ReadonlyArray<T>, U>(
    this: This, fn: (v: T) => U): { [K in keyof This]: U }
}
// }

如果我调用convertValues()(将arr参数更改为可变元组类型,这将提示编译器,如果可能的话,应该将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'.

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

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<V>的输入转换为OtherType<V>类型的输出,否则,编译器就无法注意到输入元组的每个元素与输出元组的相应元素之间的相关性。实际上,在调用map()时,可以通过注解回调来编写:

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

问题是......你如何描述这样的类属回调,它可能会做其他事情?我想你不想硬编码map()的类型来关心具体将SomeType<V>转换为OtherType<V>的回调。你想说“对于任何FG,将F<X>转换为G<X>的回调“:

// 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()的实现是否符合你声明返回的类型。
当编译器无法验证某个类型是否是你所声明的类型,而你确信你的声明是正确的,你就需要做一些类似类型Assert的事情,就像你已经做过的那样。所以我建议你继续按照你在这里展示的方式做,如果TypeScript中出现了更高级的类型,就重新审视一下。
Playground代码链接

相关问题