我有一个打印脚本的对象列表,我想确保它们的键在compile-time处是唯一的.

例如:

const obj1 = { a: 1, b: 2 };
const obj2 = { c: 3, d: 4 };
const obj3 = { e: 5, f: 6 };
const obj4 = { a: 7, g: 8 }; // 'a' is duplicated

我能够编写一个TS函数来确保任意两个对象之间的唯一性:

declare function ensureUniqueKeys<
  A,
  B extends { [K in keyof B]: K extends keyof A ? never : B[K] },
>(a: A, b: B): unknown;

ensureUniqueKeys(obj1, obj2); // no issues
ensureUniqueKeys(obj1, obj4); // Error: Types of property 'a' are incompatible.

但我想不出一种方法来处理任意的对象列表.

我一直在try 编写某种递归判断器,但它从未报告任何错误:

type EnsureUniqueKeys<T extends string[], PreviousKeys = never> = T extends [
  infer First,
  ...infer Rest,
]
  ? First extends object
    ? Rest extends string[]
      ? keyof First extends PreviousKeys
        ? never // Overlap detected, trigger a compile-time error
        : EnsureUniqueKeys<Rest, PreviousKeys | keyof First>
      : never
    : never
  : unknown;

declare function ensureUniqueKeysRecursive<T extends string[]>(...objects: EnsureUniqueKeys<T>[]): unknown;

ensureUniqueKeysRecursive(obj1, obj2, obj3, obj4); // No errors :(

再说一次:我知道这可以在运行时很容易地完成,但我需要它来判断compile-time的唯一性.

TS Playground

推荐答案

我们的目标是编写ensureUniqueKeys(),这样它就可以接受不同数量的对象参数,这样就不会知道任何参数的键存在于任何其他参数中.

我会采取的方法是制作一个mapped type over the array of inputs T,在类似数字的键I上迭代.

对于每个元素T[I],我们可以构造索引I处的T个元素except的每个元素的union个键.我们可以再次映射到T上,这一次是在类似数字的键J上迭代.我们要找的 keys 是{ [J in keyof T]: J extends I ? never : keyof T[J] }[number].

如果T[I]在该组关键字中有任何关键字K,则它是重复的关键字,并将属性设置为never.否则,我们将把它保留为T[I][K].

整件事看起来像是:

declare function ensureUniqueKeys<T extends object[] & {
  [I in keyof T]: {
    [K in keyof T[I]]: K extends {
      [J in keyof T]: J extends I ? never : keyof T[J]
    }[number] ? never : T[I][K]
  }
}>(...args: T): void;

让我们来测试一下:

const obj1 = { a: 1, b: 2 };
const obj2 = { c: 3, d: 4 };
const obj3 = { e: 5, f: 6 };
const obj4 = { a: 7, g: 8 }; // 'a' is duplicated
ensureUniqueKeys(obj1, obj2, obj3); // okay
ensureUniqueKeys(obj1, obj2, obj3, obj4); // error
//               ~~~~~~~~~~~~~~~~~~~~~~
// The types of 'a' are incompatible between these types.

看上go 不错.


为了清楚起见,让我们稍微浏览一下类型.

在第一个调用中,T被推断为[{a: number, b: number}, {c: number, d: number}, {e: number, f: number}].I迭代类数字键"0""1""2".当I"0"时,{ [J in keyof T]: J extends I ? never : keyof T[J] }[never, "c" | "d", "e" | "f"];第一个元素是never,因为J extends I在那里为真.Indexing into乘以number"c" | "d" | "e" | "f".由于T[0]在该集合中没有密钥,因此{ [K in keyof T[0]]: K extends "c" | "d" | "e" | "f" ? never : T[0][K] }仅为{a: number, b: number}.这意味着T的第一个元素是可以接受的,因为它可以赋值给自己.你可以证明另外两个元素也是可以接受的.

在第二个调用中,T被推断为[{a: number, b: number}, {c: number, d: number}, {e: number, f: number}, {a: number, g: number}].现在,当I"0"时,类型{ [J in keyof T]: J extends I ? never : keyof T[J] }[never, "c" | "d", "e" | "f", "a" | "g"],用number编制索引得到"c" | "d" | "e" | "f" | "a" | "g"].由于T[0]在该集合中具有密钥,因此{ [K in keyof T[0]]: K extends "c" | "d" | "e" | "f" | "a" | "g" ? never : T[0][K] }相当于{a: never, b: number}.这意味着T的第一个元素是not可接受的,因为{a: number, b: number}not可赋值给{a: never, b: number}.我们收到一条错误消息,说a键有问题.

Playground link to code

Typescript相关问答推荐

如何在不使用变量的情况下静态判断运算式的类型?

为什么TypeScript失go 了类型推断,默认为any?''

从typescript中的对象数组中获取对象的值作为类型'

rxjs forkJoin在错误后不会停止后续请求

TypeScript:在作为参数传递给另一个函数的记录中对函数的参数实现类型约束

基于键的值的打字动态类型;种类;

Angular 信号:当输入信号改变值时,S触发取数的正确方式是什么?

如何使用函数填充对象?

在嵌套属性中使用Required

TypeScrip:逐个 case Select 退出noUncheck kedIndexedAccess

为什么不能使用$EVENT接收字符串数组?

迭代通过具有泛型值的映射类型记录

防止重复使用 Select 器重新渲染

下载量怎么会超过下载量?

有没有一种方法可以确保类型的成员实际存在于TypeScript中的对象中?

基于泛型的条件接口键

类型实例化过深,可能是无限的&TypeScrip中的TupleUnion错误

必须从注入上下文调用Angular ResolveFn-inject()

Typescript泛型-键入对象时保持推理

在Typescript 中,是否有一种方法来定义一个类型,其中该类型是字符串子集的所有可能组合?