Home > Net >  Ensure the Uniqueness of Literal Values in a Record Type
Ensure the Uniqueness of Literal Values in a Record Type

Time:11-20

given the following code

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>

This yields correctly

type VALUESLiterals = "fieldA" | "fieldB"

Now I want to check that the literal values in my type are all unique such that

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

type VALUESLiterals = RecToDU<typeof VALUES>

will now yield never as a result instead of

type VALUESLiterals = "fieldA" | "fieldB"

as field3 als contains the literal value fieldA which is already defined by field1. So if there are dublicate literal values the whole type should be never

CodePudding user response:

If T has no duplicate property values types, then you want RecToDU<T> to be the union of all its property value types; otherwise you want it to be never. The union of all the property value types of T can be computed simply by indexing into T with the union of its keys: T[keyof T] (see this Q/A) .

So you will want RecToDU<T> to be a conditional type of the form type RecToDU<T> = XXX extends YYY ? T[keyof T] : never or perhaps type RecToDU<T> = XXX extends YYY ? never : T[keyof T]. So what will we do for XXX and YYY?

Here's one approach:

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]

Let's examine this bit:

{ [K in keyof T]-?: T[K] extends Omit<T, K>[Exclude<keyof T, K>] ? unknown : never }

What we're doing is mapping over each property key K in the keys of T, and for each property value T[K] we are comparing it to Omit<T, K>[Exclude<keyof T, K>]. The Omit<T, K> utility type produces a type that looks like T but with the K property removed; and the Exclude<X, K> utility type produces a type which filters out K from any members of the union in X. So Omit<T, K>[Exclude<keyof T, K>] is the union of all the property value types in T except for the property at key K.

For example, if T is {a: 0, b: 1, c: 2} and K is "a", then T[K] is 0. Omit<T, K> is {b: 1, c: 2}, and Exclude<keyof T, K> is "b" | "c", and so Omit<T, K>[Exclude<keyof T, K>] is 1 | 2. And so in T[K] extends Omit<T, K>[Exclude<keyof T, K>] ? ... we are comparing 0 extends 1 | 2 ? ... . This is false, but would become true if c had a value of type 0 instead of 2.

So if T[K] extends Omit<T, K>[Exclude<keyof T, K>], it means that the property value at K is duplicated in some other property also. Otherwise, it means that the property value at K is unique. Note that the rest of that conditional type is ? unknown : never, which means that for duplicate properties we produce the unknown type (the "top type" which absorbs all other types in unions), and for unique properties we produce the never type (the "bottom type" which is absorbed into all other types in unions). Again with T being {a: 0, b: 1, c: 2}, we would produce {a: never, b: never, c: never}, but for T being {a: 0, b: 1, c: 0}, we would produce {a: unknown, b: never, c: unknown}.

Now let's examine

{ [K in keyof T]-?: 
  T[K] extends Omit<T, K>[Exclude<keyof T, K>] ? unknown : never 
}[keyof T]

which is the same as before, but we're indexing into it with keyof T. So we're taking the mapped type and getting the union of its values. For {a: 0, b: 1, c: 2} this is never | never | never which is just never. But for {a: 0, b: 1, c: 0} this is unknown | never | unknown which is unknown. Since unknown absorbs all other types in unions, and never is absorbed into all other types in unions, the only way never can come out of this is if every single property is unique. If even one property value is a duplicate (uh, I guess there has to be at least two of these, maybe? maybe not, if one of these is a supertype of the other... ugh, never mind), then unknown comes out.

So we have a conditional type that evaluates to unknown if any properties are duplicated, and never if they are all unique. Hence:

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]

since unknown extends XXX is only true when XXX is itself unknown, this check is only true if T has duplicate properties, and false if T has all unique properties. For duplicate properties we return never, and for unique ones we return T[keyof T].


Whew, let's see if it works:

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

type VALUESLiterals = RecToDU<typeof VALUES>
// type VALUESLiterals = "fieldA" | "fieldB" | "fieldC"

okay, and then:

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

type VALUESLiterals = RecToDU<typeof VALUES>
// type VALUESLiterals = never

Looks good!


Note that the above implementation of RecToDU<T> was only tested with object types containing non-optional properties without index signatures and whose property values are single literal types and not unions or intersections of other types. If these conditionals are altered, then RecToDu<T> as implemented above might produce some weird or undesirable results. So be careful to test against use cases you care about, and alter the definition accordingly if necessary.

Playground link to code

  • Related