在检查值是否是联合的一部分时摆脱类型断言

Get rid of type assertion when checking if value is part of union

我有一个部分基于数组的联合类型——我也想用它来检查 运行 时间的值。但是,TypeScript 强制我在这里使用类型断言。 考虑以下代码:

const Pets = ["dog", "cat"] as const
type Pet = typeof Pets[number]

type Animal = Pet | "tiger"

function checkDanger(animal: Animal) {
  if (Pets.includes(animal as Pet)) {
        return "not dangerous"
    }
    return "very dangerous"
}

问题出在 as Pet 部分。

如果我忽略它,我会得到:Argument of type 'Animal' is not assignable to parameter of type '"dog" | "cat"'. Type '"tiger"' is not assignable to type '"dog" | "cat"'.

但是,在更复杂的现实情况下,该断言可能会产生我想要避免的影响。有没有办法在没有断言的情况下做这样的事情?

不幸的是,这是打字稿编译器的限制。但是,不用在 animal as Pet 的同一行中编写千个函数,您可以按如下方式使用类型保护

function isPet(something: Pet | Animal) : something is Pet {
    return Pets.includes(something as Pet)
} 

function checkDanger(animal: Animal) {
    if (isPet(animal)) {
        return "not dangerous"
    }
    return "very dangerous"
}

这种方法更适用,因为如果重命名类型定义名称,重构代码会更容易。

不过,如果你声明一个只读数组,你就失去了灵活性,并且必须执行类型断言。

这是 includes 的一个已知问题,请参阅 issues/26255

但是,有一个解决方法。您可以创建自定义 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)

让我们试试看:

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"
}

因为你有一个条件语句,我假设这个函数可以重载以缩小 return 类型:

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

Playground 重载签名的顺序很重要。

您可能已经注意到,我的代码中没有类型断言。

inTuple typeguard 有效,因为 tuple 被视为函数体内的字符串数组。这意味着该操作是允许的,因为 tuple[number]elem 可以相互赋值。

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)
    }