网站搜索

如何在 TypeScript 5.2.2 中正确定义一个将元组表转换为对象数组的工具函数(类似 zip),同时避免所有值的联合类型合并


我快要解决它了,但找不到绕过最后一个 TS2322: Type TcolTuple[i] is not assignable to type string | number | symbol 编译错误的方法。

所以,这里有一个工具函数 rowsToObjects(),可能很多人曾在他们的项目中定义过一两次,它的概念在某种程度上有点类似于 zip():

const objects = rowsToObjects(
    ['id', 'color' , 'shape'   , 'size'  , 'to' ] as const,  
    [  1n, 'red'   , 'circle'  , 'big'   , '0x0'] as const,
    [  2n, 'green' , 'square'  , 'small' , '0x0'] as const,
    [  3n, 'blue'  , 'triangle', 'small' , '0x0'] as const,
)

输出如下:

[
    {id: 1n, color: 'red', shape: 'circle', size: 'big', to: '0x0'},
    {id: 2n, color: 'green', shape: 'square', size: 'small', to: '0x0'},
    {id: 3n, color: 'blue', shape: 'triangle', size: 'small', to: '0x0'},
]

实际的实现显然很简单,但输入它时让我有些困难:

export function rowsToObjects<
    Tobj extends { [i in keyof TcolTuple as TcolTuple[i]]: TvalTuple[i] },
    TcolTuple extends readonly string[],
    TvalTuple extends { [j in keyof TcolTuple]: unknown }
>(cols: TcolTuple, ...rows: TvalTuple[]): Tobj[];

当前代码在我看来是合理的,但编译器对 as TcolTuple[i] 这部分报错:

TS2322: Type  TcolTuple[i]  is not assignable to type  string | number | symbol 
  Type  TcolTuple[keyof TcolTuple]  is not assignable to type  string | number | symbol 
    Type
    TcolTuple[string] | TcolTuple[number] | TcolTuple[symbol]
    is not assignable to type  string | number | symbol 
      Type  TcolTuple[string]  is not assignable to type  string | number | symbol 

我是不是错过了什么显而易见的东西?类型几乎令人满意,但没有那个 as TcolTuple[i],它无法识别哪个值属于哪个键,只会把它们全部联合起来。

答案#1

我认为你遇到的主要问题是

{ [I in keyof TcolTuple as TcolTuple[I]]: TvalTuple[I] }

这是因为使用键重映射会阻止映射的类型是同态的(参见“同态映射类型是什么意思?”),所以你映射的不是只包含元组的类似数字的索引,如"0" | "1" | "2",而是映射了所有索引,包括number,也就是所有元素的混合。这就产生了你不满意的联合类型。

这里最简单的改变是显式地只映射类似数字的索引,通过将 keyof TcolTuple 与 pattern 模板字符串类型 `$ {number}` 相交(如 microsoft/TypeScript#40598 中实现的那样)。这会去掉任何不是数字字符串的项。例如,"0" | "1" | "2" | number | "length" | "find" 与 `$ {number}` 相交后,只会得到 "0" | "1" | "2"。

这或多或少解决了问题:

declare function rowsToObjects<
  Tobj extends { [I in `${number}` & keyof TcolTuple as TcolTuple[I]]: TvalTuple[I] },
  TcolTuple extends readonly string[],
  TvalTuple extends { [J in keyof TcolTuple]: unknown }
>(cols: TcolTuple, ...rows: TvalTuple[]): Tobj[];

const objects = rowsToObjects(
  ['id', 'color', 'shape', 'size', 'to'] as const,
  [1n, 'red', 'circle', 'big', '0x0'] as const,
  [2n, 'green', 'square', 'small', '0x0'] as const,
  [3n, 'blue', 'triangle', 'small', '0x0'] as const,
)
/* const objects: {
    id: 1n | 2n | 3n;
    color: "red" | "green" | "blue";
    shape: "circle" | "square" | "triangle";
    size: "big" | "small";
    to: "0x0";
}[] */

就我个人而言,如果我是为自己写这个,我会:

  • 使用 const 类型参数,而不是要求调用者使用 const 断言;
  • 保持输入的元组类型,以便如果输入数组是严格有序的,那么输出也是严格有序的(例如,rowsToObjects(["a"],[0],[1]) 应该返回 [{a: 0}, {a: 1}] 而不是 {a: 0 | 1}[];
  • 移除多余的通用类型参数,仅在线计算输出,而不是依赖默认类型参数;
  • 对于映射类型参数使用大写字母,例如 I 和 J,而不是 i 和 j,以遵循命名约定来区分类型和变量(在 {[P in K]: F<P>} 中,P 是类型参数,而不是变量名,所以使用 p 可能会让人困惑)。

这些都不是至关重要的,但它会输出结果

declare function rowsToObjects<
  const K extends readonly PropertyKey[],
  const V extends readonly Record<keyof K, unknown>[]
>(
  cols: K, ...rows: V
): { [I in keyof V]:
    { [J in `${number}` & keyof K as K[J]]:
      V[I][J]
    }
  };

const objects = rowsToObjects(
  ['id', 'color', 'shape', 'size', 'to'],
  [1n, 'red', 'circle', 'big', '0x0'],
  [2n, 'green', 'square', 'small', '0x0'],
  [3n, 'blue', 'triangle', 'small', '0x0'],
)
/* const objects: readonly [{
    id: 1n;
    color: "red";
    shape: "circle";
    size: "big";
    to: "0x0";
}, {
    id: 2n;
    color: "green";
    shape: "square";
    size: "small";
    to: "0x0";
}, {
    id: 3n;
    color: "blue";
    shape: "triangle";
    size: "small";
    to: "0x0";
}] */

代码的 Playground 链接