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.