【问题标题】:"Structural type guard" works with `if`, but not as array filter predicate“结构类型保护”适用于 `if`,但不能用作数组过滤谓词
【发布时间】:2021-04-17 08:27:09
【问题描述】:

我有一个联合类型(在下面的示例中为Pet),它结合了多个对象类型,每个对象类型都有一个type 属性来指示它们的类型。有时我有一个联合类型 (Pet[]) 的数组,需要根据 type 属性对其进行.filter()。这本身工作得很好,但为了避免多余的类型声明,我想确保 .filter() 调用的结果自动正确输入。

User-defined type guards 似乎是解决这个问题的完美解决方案,所以我实现了一个只检查 type 属性并将类型缩小到 { type: 'something' } 显式声明完整类型(这个下面称为isCatLike)。我尝试在if 中使用它,它正确地将我的类型从Pet 缩小到Cat

然后我尝试将它用作.filter() 的谓词,这一次类型根本没有缩小。结果数组的类型仍然为Pet[],尽管我的if 实验表明类型保护通常能够从Pet 缩小到Cat

作为另一个实验,我尝试稍微更改类型保护并使类型谓词更加明确(is Cat 而不是is { type: 'cat' },突然.filter() 调用正确地缩小了@的类型 987654343@到Cat[](这个函数下面叫isCat)。

type Cat = { type: 'cat'; name: string; purrs: boolean }
type Dog = { type: 'dog'; name: string; woofs: boolean }
type Pet = Cat | Dog

declare const pets: Pet[]
const isCatLike = (pet: any): pet is { type: 'cat' } => pet.type === 'cat'
const isCat = (pet: Pet): pet is Cat => pet.type === 'cat'

for (const pet of pets) {
  if (isCatLike(pet)) {
    pet // Cat -> Correct!
  }
  if (isCat(pet)) {
    pet // Cat
  }
}

const catLikes = pets.filter(isCatLike)
catLikes // Pet[] -> Incorrect!

const cats = pets.filter(isCat)
cats // Cat[]

Open the example on the TypeScript Playground 自己检查类型。

现在的问题是我不能使用更明确的方法(由 isCat 函数说明),因为我的实际代码在联合中有更多类型,并且谓词也是 由函数 (isType(type: string))。

所以我现在想知道的是:

为什么我的“结构类型保护”在if 语句的上下文中起作用,而不是作为过滤数组的谓词?在这两种情况下它不应该以完全相同的方式工作吗?我做错了什么还是我遇到了类型系统的限制?

【问题讨论】:

    标签: typescript types structural-typing


    【解决方案1】:

    这归结为Array.filter 的类型是如何编写的:

    interface Array<T>
        // This is the signature that enables the array to be a different type
        filter<S extends T>(predicate: (value: T, index: number, array: T[]) => value is S, thisArg?: any): S[];
    
        // Normal signature that returns the same type of array
        filter(predicate: (value: T, index: number, array: readonly T[]) => unknown, thisArg?: any): T[];
    }
    

    该签名的密钥位是&lt;S extends T&gt;,在这种情况下,TPetS { type: "cat" }。但是{ type: "cat" }没有扩展Pet,所以签名不适用,所以属于普通的过滤器签名。


    它适用于单个元素的情况,因为 TS 的缩小逻辑实际上比.filter 的签名中表达的要聪明一些——它实际上结合了类型保护和原始类型的结果——类似于@987654331 @ 与Cat 相同。

    【讨论】:

      【解决方案2】:

      来自手册https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards

      pet is Fish 是本例中的类型谓词。谓词采用 parameterName is Type 的形式,其中 parameterName 必须是当前函数签名中的参数名称。任何时候使用某个变量调用 isFish 时,TypeScript 都会将该变量缩小到该特定类型如果原始类型兼容

      我猜由于类型保护中的原始类型是any,TS 根本无法缩小范围,如果你将any 换成Cat,你甚至会得到一个编译错误,说@987654325 @ 不能分配给 Cat 导致缺少其他两个道具。

      我认为这是一个提示,并将其他道具设为可选: type Cat = { type: 'cat'; name?: string; purrs?: boolean }

      然后,您的谓词开始按您的意愿工作。

      【讨论】:

      • “我猜因为原始类型是类型保护中的 any,所以 TS 根本无法缩小范围” — 这不正是它所做的吗在if 里面?我明白你为什么说 {type: 'cat'} 不能分配给 Cat 导致缺少其他两个道具”,但为什么这突然不成为 if 内部的问题?这就是我感到困惑的地方。
      • 它知道在您的Pets 数组中,只能有狗或猫。它知道{type: 'cat'} 类型不能分配给DogCat,所以当使用该类型保护进行过滤时,结果仍然只是Pets 的数组,
      • 我明白这一点,但if 有什么不同?我不能对那里的pet 变量说同样的话吗?它只能是CatDog 类型? TypeScript 在那里使用完全相同的“不完整”类型保护,但不知何故得出了 不同 的结论,即pet 实际上 在该上下文中属于Cat 类型。我觉得 TypeScript 在这里缩小类型的方式有点不一致,不是吗?
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-04-13
      相关资源
      最近更新 更多