TypeScript编写泛型zipN函数遇错误:Mapped type非数组类型
zipN函数遇到的类型错误 我在实现TypeScript泛型zipN函数时碰到了问题,代码最后一行报错:Type 'ZipN<unknown[][]>' is not an array type。
为了避免ZipN生成的对象包含原型方法这类非索引属性,我定义了OnlyNumeric类型并作用于keyof LongestN<T>,但结果ZipN生成的是带数字键的普通对象,不是预期的数组/元组类型。但奇怪的是,{ [L in keyof T]: MaybeIndex<T[L], K> }明明能生成数组类型,我搞不懂问题出在哪。
代码如下:
type Keys<T> = { [K in keyof T]: K }; type Longest2<T extends unknown[], U extends unknown[]> = Keys<T> extends [...Keys<U>, ...infer _] ? T : U; type LongestN<T extends unknown[][]> = T extends [infer T0 extends unknown[], ...infer Ts extends unknown[][]] ? Longest2<T0, LongestN<Ts>> : T[0]; type OnlyNumeric<T> = T extends `${number}` ? T : never; type MaybeIndex<T, K> = K extends keyof T ? T[K] : never; type ZipN<T extends unknown[][]> = { [K in OnlyNumeric<keyof LongestN<T>>]: { [L in keyof T]: MaybeIndex<T[L], K> } }; export const zipN = <T extends unknown[][]>(...[head, ...tail]: T): ZipN<T> => [head, ...zipN(...tail) as ZipN<typeof tail>] as any;
问题根源
TypeScript里,只有当索引签名是number类型,并且类型符合数组的结构特征时,才会被识别为数组/元组类型。你用OnlyNumeric<keyof LongestN<T>>作为索引,这里的OnlyNumeric返回的是字符串类型的数字(比如"0"、"1"),而非number类型。所以ZipN最终生成的是带有字符串数字键的对象,不是数组。
另外,keyof LongestN<T>对于数组来说会包含所有数字索引的字符串形式("0"、"1"...)以及数组原型上的属性名(比如"length"、"push"),你用OnlyNumeric过滤后剩下的是字符串数字键,但这依然是字符串类型,不符合数组要求的number索引规则。
修复方案
要让ZipN生成数组/元组类型,需要把索引从字符串数字转为number类型,同时利用TypeScript的元组推导特性:
步骤1:重构ZipN类型
通过生成指定长度的元组来推导数字索引,再基于这些索引映射出最终的数组类型:
// 生成长度为N的元组,用于推导数字索引 type TupleOfLength<N extends number, T = unknown, R extends T[] = []> = R['length'] extends N ? R : TupleOfLength<N, T, [...R, T]>; // 获取最长数组的长度 type LongestLength<T extends unknown[][]> = LongestN<T>['length']; // 重构ZipN:基于最长长度的元组索引进行映射 type ZipN<T extends unknown[][]> = { [K in keyof TupleOfLength<LongestLength<T>>]: { [L in keyof T]: MaybeIndex<T[L], K & number> } };
步骤2:修正LongestN的边界情况
原LongestN在处理空数组时返回T[0],会得到undefined,需要调整为返回空数组:
type LongestN<T extends unknown[][]> = T extends [infer T0 extends unknown[], ...infer Ts extends unknown[][]] ? Longest2<T0, LongestN<Ts>> : [];
完整修复后的代码
type Keys<T> = { [K in keyof T]: K }; type Longest2<T extends unknown[], U extends unknown[]> = Keys<T> extends [...Keys<U>, ...infer _] ? T : U; type LongestN<T extends unknown[][]> = T extends [infer T0 extends unknown[], ...infer Ts extends unknown[][]] ? Longest2<T0, LongestN<Ts>> : []; type MaybeIndex<T, K> = K extends keyof T ? T[K] : never; // 生成指定长度的元组 type TupleOfLength<N extends number, T = unknown, R extends T[] = []> = R['length'] extends N ? R : TupleOfLength<N, T, [...R, T]>; type LongestLength<T extends unknown[][]> = LongestN<T>['length']; // 重构后的ZipN类型 type ZipN<T extends unknown[][]> = { [K in keyof TupleOfLength<LongestLength<T>>]: { [L in keyof T]: MaybeIndex<T[L], K & number> } }; export const zipN = <T extends unknown[][]>(...[head, ...tail]: T): ZipN<T> => { if (tail.length === 0) return head.map(item => [item]) as ZipN<T>; const zippedTail = zipN(...tail); return head.map((item, idx) => [item, ...(zippedTail[idx] || [])]) as ZipN<T>; };
修复效果
现在ZipN会生成正确的数组/元组类型,比如调用zipN([1,2], ['a','b','c'])时,返回类型是[[1, 'a'], [2, 'b'], [undefined, 'c']],符合预期,也不会再出现数组类型错误。
内容的提问来源于stack exchange,提问作者user8649828

