TypeScript 5.1.6显式推导元组类型并检测重复键的问题
TypeScript 元组推导丢失与重复键检测问题
环境与基础定义
TypeScript版本:5.1.6
首先定义类C,其第一个泛型参数为对象类型:
declare class C<a = unknown, b = unknown> { getA(): a; getB(): b; };
正常元组推导场景
原本通过以下写法可以让函数f正确将传入的C实例数组推导为元组类型:
declare function f<T extends any[]>(arr: [...T]): any; declare const a: C<{ name: string }, []>; declare const b: C<{ name: string }, []>; f([a, b]) // 悬停可看到正确的元组类型:[C<{ name: string }, []>, C<{ name: string }, []>]
问题:加入重复键检测后元组类型丢失
当尝试通过类型工具检测元组中C实例的第一个泛型参数是否存在重复键时,元组类型会丢失,被推导为数组类型(C<{ name: string }, []>[])[]。以下是两种尝试的写法:
写法一
type ExtractObject<I> = I extends C<infer P> ? P : never; type ExtractObjectFromList<I> = { [P in keyof I]: ExtractObject<I[P]>; }; declare function f<T extends any[]>(arr: HasDuplicateKey<ExtractObjectFromList<[...T]>> extends true ? "Err" : T): any; declare const a: C<{ name: string }, []>; declare const b: C<{ name: string }, []>; f([a, b]) // 类型变为数组而非元组
写法二
declare function f<T extends any[]>(arr: [...T] extends infer P ? HasDuplicateKey<ExtractObjectFromList<P>> extends true ? "err" : P : never): any; declare const a: C<{ name: string }, []>; declare const b: C<{ name: string }, []>; f([a, b]) // 同样丢失元组类型
需求
- 无需使用
as const,保持库的向后兼容性 - 当
C的第一个泛型参数存在键冲突时,函数参数类型报错 - 支持扩展更多额外类型检查
附HasDuplicateKey类型定义:
export type HasDuplicateKey<T> = T extends readonly [infer First, ...infer Rest] ? Rest extends readonly [infer Next, ...infer Others] ? keyof First extends keyof Next ? true : HasDuplicateKey<readonly [First, ...Others]> : false : false;
解决方案
问题出在条件类型的分布性以及类型推导时的“宽化”行为。我们可以通过保留参数位置的元组约束,并把类型检查移到独立类型逻辑中,或者使用非分布条件类型避免元组被宽化为数组。
方案1:非分布条件类型+元组约束
修改函数定义,将重复键检查封装为独立类型,同时保留参数的元组推导能力:
type ExtractObject<I> = I extends C<infer P> ? P : never; type ExtractObjectFromList<I> = { [P in keyof I]: ExtractObject<I[P]>; }; // 用[]包裹条件类型,避免分布性拆分元组 type IsValidTuple<T extends any[]> = [HasDuplicateKey<ExtractObjectFromList<T>>] extends [true] ? never : T; declare function f<T extends any[]>(arr: [...IsValidTuple<T>]): any; declare const a: C<{ name: string }, []>; declare const b: C<{ name: string }, []>; declare const c: C<{ age: number }, []>; f([a, b]) // 报错:类型不匹配(检测到重复键) f([a, c]) // 正常推导为元组类型:[C<{ name: string }, []>, C<{ age: number }, []>]
方案2:返回值类型做检查(更灵活)
如果需要保留参数的正常推导,同时在存在重复键时报错,可以把检查逻辑移到返回值类型:
type ExtractObject<I> = I extends C<infer P> ? P : never; type ExtractObjectFromList<I> = { [P in keyof I]: ExtractObject<I[P]>; }; declare function f<T extends any[]>(arr: [...T]): HasDuplicateKey<ExtractObjectFromList<T>> extends true ? never : T; declare const a: C<{ name: string }, []>; declare const b: C<{ name: string }, []>; declare const c: C<{ age: number }, []>; f([a, b]) // 返回值类型为never,触发类型错误 f([a, c]) // 返回值类型为正确的元组
方案说明
- 非分布条件类型
[X] extends [Y]避免了TypeScript将元组拆分为单个元素处理,从而保留元组结构 - 参数位置的
[...T]仍然会触发TypeScript将数组字面量推导为元组,而非宽化为普通数组 - 两种方案都不需要
as const,完全兼容现有调用方式
内容的提问来源于stack exchange,提问作者Hello Hello
相关产品推荐
相关产品推荐

