You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 13:56:05