Issue
I want to create a typescript
type that checks whether all of an existing string enum
values are used in another const. for example:
enum VALUES{
val1 = 'val1',
val2 = 'val2'
}
enum KEYS{
key1= 'key1',
key2= 'key2'
}
const mapping: { readonly [i in KEYS]: VALUES[] } = {
[KEYS.key1]: [VALEUS.value1],
[KEYS.key2]: [VALEUS.value2, VALEUS.value1],
}
In the mapping
const, if a new key is added to KEYS
enum, a compile error will occur and this new key must be used in mapping const.
I want to enrich the mapping
const type so that if a new value is added to VALUES enum, a compile error occurs which notify developer that this new value must be used too.
For example if value3 is added, it must be used inside either of key1 or key2 values or a compile error must rise. Something like this is desired after adding value3 into VALUES enum:
const mapping: { readonly [i in KEYS]: VALUES[] } = {
[KEYS.key1]: [VALEUS.value1,VALEUS.value3],
[KEYS.key2]: [VALEUS.value2, VALEUS.value1],
}
I do not know how to specify the related type for mapping
.
My typescript
version is 4.9 and It can't be upgraded anymore.
Solution
To check whether all enum values are used we will first need to understand how to compare types.
type Case1 = 'a' | 'b' | 'c' extends 'a' ? true : false; // false
type Case2 = 'a' extends 'a' | 'b' | 'c' ? true : false; // true
For the condition to be true the left part should be more specific than the right side. In Case1
the left side is not specific since it can be either a
, b
, or c
, whereas the right side is exactly a
which is specific, this condition is not true.
Knowing this we should figure out what should be on the left side and on the right side in our case. On one side there will be values of the enum and on the other one values from the mapping in a union format. If some values will be missing in the mapping then the union will be more specific and that's why the mapping values should be on the right side. Otherwise, the condition will be always true, since all values are less specific than the values in the mapping.
To get the values we will use this utility type:
type ValueOf<T> = T[keyof T];
Let's write a type that will accept a mapping and by using the ValueOf
get the expected condition:
type Validate<T extends Record<KEYS, readonly VALUES[]>> = ValueOf<
typeof VALUES
> extends ValueOf<ValueOf<T>>
? T
: never;
So if the condition is true, we return the mapping itself, otherwise never
. To make this whole thing work we will need a dummy generic function to enable the type-checking:
const dummy = <T extends Record<KEYS, readonly VALUES[]>>(
mapping: Validate<T>,
) => mapping;
Since you are not using Typescript 5.0 >=
you won't be able to use const type parameters, which are necessary to prevent the compiler from widening the mapping's type, but fortunately you still can use const assertion to make the magic happen.
Testing:
const mapping = dummy({
[KEYS.key1]: [VALUES.val2], // val1 is missing expected error
[KEYS.key2]: [VALUES.val2],
} as const);
const mapping2 = dummy({
[KEYS.key1]: [VALUES.val1], // no error
[KEYS.key2]: [VALUES.val2],
} as const);
Answered By - wonderflame
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.