如何在 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 链接
