enum AllowedFruits {
Apple = 'APPLE',
Banana = 'BANANA',
Pear = 'PEAR'
}
const allowedFruits: AllowedFruits[] = [
AllowedFruits.Apple, AllowedFruits.Banana, AllowedFruits.Pear
]
What I want to achieve is restricting an array to have every field of specific enum.
I expect allowedFruits
shows type error by adding or removing field of AllowedFruits
.
Is there any way to achieve it?
If there are any articles or documents that I can refer to please let me know.
CodePudding user response:
Option 1
We can solve this by creating a type containing all possible combinations of AllowedFruits
.
type AllCombinations<T extends string> = [T] extends [never]
? []
: {
[K in T]: [K, ...AllCombinations<Exclude<T, K>>]
}[T]
type AllFruitCombinations = AllCombinations<AllowedFruits>
This may result in bad performance if you have a lot of elements inside the enum because every single combination needs to be calculated first.
Let's see if this works:
/* Error */
const t1: AllFruitCombinations = []
const t2: AllFruitCombinations = [AllowedFruits.Apple]
const t3: AllFruitCombinations = [AllowedFruits.Apple, AllowedFruits.Banana]
const t4: AllFruitCombinations = [AllowedFruits.Apple, AllowedFruits.Banana, AllowedFruits.Pear, AllowedFruits.Pear]
/* OK */
const t5: AllFruitCombinations = [AllowedFruits.Apple, AllowedFruits.Banana, AllowedFruits.Pear]
Option 2
It is also possible to solve this by passing allowedFruits
to a function with a generic type.
We can create a generic helper type ExhaustiveFruits
which checks if all enum values are present in the array.
type ExhaustiveFruits<
O extends AllowedFruits[],
T extends AllowedFruits[] = O,
P extends string = `${AllowedFruits}`
> = [P] extends [never]
? O
: T extends [`${infer L}`]
? [P] extends [L]
? O
: never
: T extends [`${infer L}`, ...infer R]
? R extends AllowedFruits[]
? ExhaustiveFruits<O, R, Exclude<P, L>>
: never
: never
The logic of ExhaustiveFruits
is quite simple: It is a recursive type where we start with a union of all enum values as P
and the tuple of AllowedFruits
as T
.
For each element of T
, the string
value of the element is inferred with '${infer L}'
. Afterwards this value is removed from the P
union with Exclude<P, L>
.
Every iteration there is a check if P
is empty with [P] extends [never]
or if the last element of T
is the last element of P
with [P] extends [L]
. If this is the case, the original tuple O
can be returned. If T
is empty but P
has still AllowedFruits
in its union, never
is returned.
The type can be used in a generic function createAllowedFruitsArray
like this:
function createAllowedFruitsArray<
T extends AllowedFruits[]
>(arr: [...ExhaustiveFruits<T>]) : T {
return arr
}
Some checks to see if this is working:
createAllowedFruitsArray(
[] // Error
)
createAllowedFruitsArray(
[AllowedFruits.Apple] // Error
)
createAllowedFruitsArray(
[AllowedFruits.Apple, AllowedFruits.Banana] // Error
)
createAllowedFruitsArray(
[AllowedFruits.Apple, AllowedFruits.Banana, AllowedFruits.Pear] // OK
)
Right now it would also be possible to use the same enum value multiple times, as long as all are used.
createAllowedFruitsArray(
[AllowedFruits.Apple,
AllowedFruits.Banana,
AllowedFruits.Pear,
AllowedFruits.Pear] // Also ok, even though Pear is twice in the array
)
But with a slight modification, we can also change this:
type ExhaustiveFruits<
O extends AllowedFruits[],
T extends AllowedFruits[] = O,
P extends string = `${AllowedFruits}`
> = [P] extends [never]
? O["length"] extends 0
? O
: never
: T extends [`${infer L}`]
? [P] extends [L]
? O
: never
: T extends [`${infer L}`, ...infer R]
? R extends AllowedFruits[]
? [L] extends [P]
? ExhaustiveFruits<O, R, Exclude<P, L>>
: never
: never
: never