# 30178-UniqueItems
# 题目描述
实现 uniqueItems——一个"受约束的恒等函数"(Constrained Identity Function, CIF)。它接收一个字面量元组,
- 元素全部唯一时,返回这个元组(类型上保持
const精度); - 元素有重复时,让重复的那些位置直接报类型错误。
uniqueItems([1, 2, 3]); // OK,返回类型 [1, 2, 3]
uniqueItems([1, 1]); // 报错
uniqueItems([1, 2, 2, 3, 4, 4]);
// 期望:只有两个 2 和两个 4 的位置报错,其他正常
加分项:
- 定位到具体重复元素的下标位置报错;
- 友好的错误信息,而非冷冰冰的
not assignable to never。
# 分析
关键技巧是 CIF 的经典模板:
- 用
<const T extends readonly unknown[]>的const修饰让 TS 把输入推断为最窄的字面量元组。 - 参数的静态类型不是
T,而是T & MarkDupes<T>——这是一种约束"回贴":对T逐位置打标记,重复位置的类型打成never,唯一位置保持原样。 - 返回值类型仍然是
T,保证调用方拿到的是原始精度。
计数"这个元素在元组里出现了几次"可以用最朴素的递归遍历,判等必须用 Equal 的"严格相等"版本(见 判断两个类型相等),否则 any / unknown / never / 宽类型会互相吸收,导致误判。
# 题解
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y
? 1
: 2
? true
: false;
// 检查 E 是否已经出现在 Seen 里
type AnyEqual<Seen extends readonly unknown[], E> = Seen extends readonly [
infer F,
...infer R,
]
? Equal<F, E> extends true
? true
: AnyEqual<R, E>
: false;
// 对元组一边推进、一边维护已看过的元素;后出现的重复项打成 never
type MarkDupes<
T extends readonly unknown[],
Seen extends readonly unknown[] = [],
> = T extends readonly [infer F, ...infer R]
? AnyEqual<Seen, F> extends true
? [never, ...MarkDupes<R, [...Seen, F]>]
: [F, ...MarkDupes<R, [...Seen, F]>]
: [];
declare function uniqueItems<const T extends readonly unknown[]>(
items: T & MarkDupes<T>,
): T;
解读:
<const T extends readonly unknown[]>:const让 TS 把传入的数组字面量推断成最精细的只读元组(不带const会退化成宽类型number[]、string[]等),这是整个类型约束能生效的前提。MarkDupes<T, Seen>:一个带累加器的递归。从左往右扫元组:当前元素如果已出现在Seen里,就把这个位置打成never;否则保留原类型,并把它加入Seen继续递归。效果是第一次出现的元素合法、后续重复处才报错——这样@ts-expect-error只需标注在第二个 / 第三个等重复位置即可。AnyEqual<Seen, E>:严格Equal逐支扫描Seen,避免any/unknown等宽类型互相吸收导致的误判。- 参数类型
T & MarkDupes<T>:用交叉把原元组和"位置标记版"绑起来。调用方传的[1, 2, 2]在位置 2 上的MarkDupes结果是never,和2不兼容,TS 就报错;不重复的位置交叉后等于原类型,传入 OK。 - 返回类型仍是
T,保留字面量精度。
进一步的友好错误信息(比如"重复元素不允许"这类文案)可以把 never 换成带 brand 的标签类型,例如 { _error: 'Duplicate element' },但会牺牲 @ts-expect-error 写法的简洁,这里不展开。
# 验证
uniqueItems([1, 2, 3]); // OK
uniqueItems(['a', 'b', 'c']); // OK
uniqueItems([1, 'a', true]); // OK
// @ts-expect-error 两个 1
uniqueItems([1, 1]);
uniqueItems([
1, 2,
// @ts-expect-error 第二个 2
2, 3, 4,
// @ts-expect-error 第二个 4
4,
]);
# 知识点
- CIF(受约束的恒等函数)是把"运行时接受参数"和"编译时检查约束"拧在一起的通用套路:把静态约束写进参数类型里,TS 不满足就报错。
<const T>是 TypeScript 5.0 引入的 const 修饰泛型,效果相当于在调用点加as const。没有它,数组字面量会被推成宽类型元组,字面量级的约束全部失效。- 对每一位独立打标记的 mapped type
[K in keyof T]: ...,在元组上保留元组形态。配合T & MarkDupes<T>产生位置级错误,是比"整体 never"更友好的体验。 - 严格
Equal写法见 判断两个类型相等;带计数元组的递归统计见 进阶-计数-加减乘除。