This particular issue is a well-documented one that involves the use of includes
. For more details, you can refer to the discussion on issues/26255.
Fortunately, there exists a workaround for this problem. You have the option to create a custom curried
typeguard:
const inTuple = <Tuple extends string[]>(
tuple: readonly [...Tuple]) => (elem: string
): elem is Tuple[number] =>
tuple.includes(elem)
// (elem: string) => elem is "dog" | "cat"
const inPets = inTuple(Pets)
Let's put it into practice:
const Pets = ["dog", "cat"] as const
type Pet = typeof Pets[number]
type Animal = Pet | "tiger"
const inTuple = <Tuple extends string[]>(
tuple: readonly [...Tuple]) => (elem: string
): elem is Tuple[number] =>
tuple.includes(elem)
// (elem: string) => elem is "dog" | "cat"
const inPets = inTuple(Pets)
function checkDanger(animal: Animal) {
if (inPets(animal)) {
animal // "dog" | "cat"
return "not dangerous"
}
return "very dangerous"
}
Given the presence of a conditional statement, there may be an opportunity to enhance the function through overloading and narrowing the return type:
const Pets = ["dog", "cat"] as const
type Pet = typeof Pets[number]
type Animal = Pet | "tiger"
const inTuple = <Tuple extends string[]>(
tuple: readonly [...Tuple]) => (elem: string
): elem is Tuple[number] =>
tuple.includes(elem)
// (elem: string) => elem is "dog" | "cat"
const inPets = inTuple(Pets)
function checkDanger(animal: Pet): "not dangerous"
function checkDanger(animal: Animal): "very dangerous"
function checkDanger(animal: string) {
if (inPets(animal)) {
animal // "dog" | "cat"
return "not dangerous"
}
return "very dangerous"
}
const result = checkDanger('tiger') // very dangerous
const result2 = checkDanger('cat') // not dangerous
Interactive Playground
The order of overload signatures plays a crucial role.
You may have observed that there are no explicit type assertions within my code.
The functionality of the inTuple
typeguard stems from the fact that inside the function body, tuple
is treated as an array of strings. This allows the operation to be valid, since tuple[number]
and elem
are mutually assignable.
const inTuple = <Tuple extends string[]>(
tuple: readonly [...Tuple]) =>
(elem: string): elem is Tuple[number] => {
tuple[2] = elem // ok
elem = tuple[3] // ok
return tuple.includes(elem)
}