为何TypeScript中看似相同的元组类型不相等?
TypeScript元组UnNullify类型与Equals测试失败的原因及解决方法
问题背景
在TypeScript中操作元组时,可选元素会被自动追加undefined类型(例如[a: 1, b?: 2]会被隐式转为[a: 1, b?: 2 | undefined]),可读性较差。为此编写了UnNullify类型,用于移除元组元素中冗余的null和undefined:
// 依赖Playground中的Cast类型,确保输入为ReadonlyArray type UnNullify<Data extends ReadonlyArray<any>> = Cast<{ [K in keyof Data]: Exclude<Data[K], null | undefined> }, ReadonlyArray<any>>
该类型的计算结果在类型提示中显示为[a: 1, b?: 2],但与手写的同结构元组无法通过Equals类型相等性测试:
type T = UnNullify<[a: 1, b?: 2 | undefined]> // 类型提示显示为 [a: 1, b?: 2] type Test1 = Equals<T, [a: 1, b?: 2]> // 返回false type Test2 = Equals<[a: 1, b?: 2], [a: 1, b?: 2]> // 返回true(符合预期)
原因分析
这是TypeScript的已知问题(对应Bug报告#50753):当通过映射数组索引的方式处理元组时,TypeScript无法保留元组可选元素的原始语义标记。
- 手写的
[a: 1, b?: 2]中,b是显式标记的可选元素,TypeScript内部会将其记录为元组的可选位置。 - 而
UnNullify通过映射生成的元组,b元素是通过Exclude移除undefined后,由TypeScript自动推断为“可选”,但内部实际标记为「包含undefined的必填元素」,只是在类型提示中简化显示为可选形式。Equals类型会严格对比类型的内部标记,而非仅表面的结构等价,因此返回false。
解决方法
需要绕过映射数组索引的方式,直接针对元组的元素和可选修饰符进行构造,保留原始的可选元素标记。修改后的UnNullify类型可以利用元组的递归推断实现:
// 依赖Cast类型确保输入为元组 type UnNullify<T extends ReadonlyArray<any>> = Cast< T extends readonly [infer First, ...infer Rest] ? First extends undefined | null ? never // 处理全为null/undefined的元素(按需调整) : [Exclude<First, null | undefined>, ...UnNullify<Rest>] : T extends readonly [infer First?, ...infer Rest] ? [Exclude<First, null | undefined>?, ...UnNullify<Rest>] : [], ReadonlyArray<any> >
测试验证:
type T = UnNullify<[a: 1, b?: 2 | undefined]> // [a: 1, b?: 2] type Test1 = Equals<T, [a: 1, b?: 2]> // 返回true(符合预期)
这种方式通过递归拆解元组,分别处理必填和可选的元素位置,直接构造带有正确可选标记的元组,从而通过Equals的严格相等测试。
内容的提问来源于stack exchange,提问作者zedryas
相关产品推荐
相关产品推荐

