确保记录类型中文字值的唯一性

rob*_*kuz 3 typescript mapped-types

给出以下代码

const VALUES = {
    field1: "fieldA",
    field2: "fieldB",
} as const


export type RecToDU<T> = {
    [K in keyof T]: T[K]
}[keyof T]

type VALUESLiterals = RecToDU<typeof VALUES>
Run Code Online (Sandbox Code Playgroud)

这会正确产生

type VALUESLiterals = "fieldA" | "fieldB"
Run Code Online (Sandbox Code Playgroud)

现在我想检查我的类型中的文字值是否都是唯一的,以便

const VALUE = {
    field1: "fieldA",
    field2: "fieldB",
    field3: "fieldA" // "fieldA" is again a literal value
} as const

type VALUESLiterals = RecToDU<typeof VALUES>
Run Code Online (Sandbox Code Playgroud)

现在将产生never结果而不是

type VALUESLiterals = "fieldA" | "fieldB"
Run Code Online (Sandbox Code Playgroud)

as als 包含已由 定义的field3文字值。因此,如果存在重复的文字值,则整个类型应该是fieldAfield1never

jca*_*alz 5

如果T没有重复的属性值类型,那么您希望RecToDU<T>成为其所有属性值类型的并集;否则你希望它是never。的所有属性值类型的并集T可以简单地通过使用其键的并集进行索引来计算:( 请参阅此 Q/A)。TT[keyof T]

因此,您将希望RecToDU<T>成为以下形式的条件类型type RecToDU<T> = XXX extends YYY ? T[keyof T] : never或也许type RecToDU<T> = XXX extends YYY ? never : T[keyof T]XXX那么我们要为和做什么YYY呢?

这是一种方法:

type RecToDU<T> = unknown extends {
   [K in keyof T]-?: T[K] extends Omit<T, K>[Exclude<keyof T, K>] ? unknown : never
}[keyof T] ? never : T[keyof T]
Run Code Online (Sandbox Code Playgroud)

让我们检查一下这一点:

{ [K in keyof T]-?: T[K] extends Omit<T, K>[Exclude<keyof T, K>] ? unknown : never }
Run Code Online (Sandbox Code Playgroud)

我们正在做的是映射K的键中的每个属性键T,并且对于每个属性值,T[K]我们将其与 进行比较Omit<T, K>[Exclude<keyof T, K>]实用程序Omit<T, K>类型生成一个看起来类似TK删除了属性的类型;并且实用Exclude<X, K>程序类型K生成一个从 中的联合的任何成员中过滤掉的类型Xkey 处的属性Omit<T, K>[Exclude<keyof T, K>]外,所有属性值类型的并集也是如此。T K

例如,如果Tis{a: 0, b: 1, c: 2}Kis "a",则T[K]is 0Omit<T, K>{b: 1, c: 2}Exclude<keyof T, K>是,"b" | "c"也是Omit<T, K>[Exclude<keyof T, K>]1 | 2所以T[K] extends Omit<T, K>[Exclude<keyof T, K>] ? ...我们正在比较0 extends 1 | 2 ? ... c这是错误的,但如果有 type 的值0而不是 ,则该值将变为 true 2

因此,如果T[K] extends Omit<T, K>[Exclude<keyof T, K>],则意味着 at 的属性值K也在其他某个属性中重复。否则,就意味着at的属性值K是唯一的。请注意,条件类型的其余部分是? unknown : never,这意味着对于重复属性,我们生成类型unknown吸收联合中所有其他类型的“顶部类型”),对于唯一属性,我们生成类型(“底部类型”,它吸收联合中的所有其他类型never被吸收工会中的所有其他类型中)。同样,对于T存在{a: 0, b: 1, c: 2},我们会生产{a: never, b: never, c: never},但对于T存在{a: 0, b: 1, c: 0},我们会生产{a: unknown, b: never, c: unknown}

现在让我们检查一下

{ [K in keyof T]-?: 
  T[K] extends Omit<T, K>[Exclude<keyof T, K>] ? unknown : never 
}[keyof T]
Run Code Online (Sandbox Code Playgroud)

这与以前相同,但我们使用 对其进行索引keyof T。因此,我们采用映射类型并获取其值的并集。因为{a: 0, b: 1, c: 2}这就是never | never | never那只是never。但对于{a: 0, b: 1, c: 0}这就是unknown | never | unknown哪是unknown。由于unknown吸收了联合中的所有其他类型,并被never吸收到联合中的所有其他类型中,因此唯一的方法never就是每个属性都是唯一的。如果甚至有一个属性值是重复的(呃,我猜必须至少有两个,也许?也许不是,如果其中一个是另一个的超类型......呃,没关系),然后unknown出来。

因此,我们有一个条件类型,用于评估unknown是否有任何属性重复以及never它们是否都是唯一的。因此:

type RecToDU<T> = unknown extends {
   [K in keyof T]-?: T[K] extends Omit<T, K>[Exclude<keyof T, K>] ? unknown : never
}[keyof T] ? never : T[keyof T]
Run Code Online (Sandbox Code Playgroud)

由于unknown extends XXX仅当XXXis 本身时才为 true,因此仅当具有重复属性时unknown此检查才为 true ,而如果具有所有唯一属性则为 false。对于重复的属性,我们返回,对于唯一的属性,我们返回。TTneverT[keyof T]


哇,让我们看看它是否有效:

const VALUES = {
   field1: "fieldA",
   field2: "fieldB",
   field3: "fieldC"
} as const

type VALUESLiterals = RecToDU<typeof VALUES>
// type VALUESLiterals = "fieldA" | "fieldB" | "fieldC"
Run Code Online (Sandbox Code Playgroud)

好的,然后:

const VALUES = {
   field1: "fieldA",
   field2: "fieldB",
   field3: "fieldA"
} as const

type VALUESLiterals = RecToDU<typeof VALUES>
// type VALUESLiterals = never
Run Code Online (Sandbox Code Playgroud)

看起来不错!


请注意,上述实现RecToDU<T>仅使用包含不带索引签名的非可选属性的对象类型进行测试,并且其属性值是单个文字类型,而不是其他类型的并集或交集。如果这些条件被改变,那么上面的实现可能会产生一些奇怪的或不需要的结果。因此,请小心测试您关心的用例,并在必要时相应地更改定义。RecToDu<T>

Playground 代码链接