TypeScript中解构后如何保留数组的联合元组类型?
先看这个场景:
const entries = [["a", 1], ["b", 2]] as (["a", 1] | ["b", 2])[]; const mapped = entries.map((e) => e);
这里entries是联合元组数组,mapped会完整保留原类型(["a", 1] | ["b", 2])[]。
但如果在map回调里解构元组:
const mapped = entries.map(([k, v]) => [k, v] as const);
此时mapped的类型会变成["a" | "b", 1 | 2][]——TypeScript会把联合元组的成员类型合并,丢失了原有的元组配对关系。
我们需要让map的输出类型和输入保持一致的联合元组结构,同时支持对每个元组成员做强类型转换,比如:
// 基于内置Uppercase工具类型 entries.map(([k, v]) => [k.toUpperCase(), v] as [Uppercase<typeof k>, typeof v]); // 预期输出类型:(["A", 1] | ["B", 2])[]
解决方案
方案1:自定义Array.map的类型签名(优先)
TypeScript默认的Array.map类型签名没有针对联合类型做分布式处理,我们可以扩展全局Array接口,让它支持保留联合元组的个体类型:
declare global { interface Array<T> { map<U>( callbackfn: <Item extends T>(value: Item, index: number, array: T[]) => U, thisArg?: any ): { [Idx in keyof this]: ReturnType<typeof callbackfn> extends (arg: this[Idx]) => infer R ? R : never }; } }
这个签名通过泛型Item extends T让回调函数针对联合类型的每个成员单独处理,再通过索引类型映射把每个元素的转换结果对应到数组的每个位置,最终保留联合元组的结构。
方案2:在回调函数中使用泛型约束
如果不想修改全局类型,可以直接在map的回调里定义泛型,明确约束每个元素是原数组的联合成员:
const mapped = entries.map(<T extends (typeof entries)[number]>([k, v]: T) => [k.toUpperCase(), v] as [Uppercase<T[0]>, T[1]] );
这里T extends (typeof entries)[number]让每个回调处理的是联合类型中的单个元组,而非整个联合,因此Uppercase<T[0]>和T[1]会保留原元组的配对关系,最终输出类型就是(["A", 1] | ["B", 2])[]。
原理说明
TypeScript对联合类型的处理默认是合并宽化,当回调函数接收的是整个联合类型时,会把所有成员的同位置类型合并成联合;而通过泛型让回调针对每个联合成员单独处理,就能触发分布式条件类型的特性,保留每个元组的具体结构,从而让输出数组的类型保持原联合元组的配对关系。
内容的提问来源于stack exchange,提问作者3x071c

