简体   繁体   English

“结构类型保护”适用于 `if`,但不能用作数组过滤谓词

[英]“Structural type guard” works with `if`, but not as array filter predicate

I have a union type ( Pet in the example below) that combines multiple object types which each have a type property indicating their type.我有一个联合类型(在下面的示例中为Pet ),它结合了多个 object 类型,每个类型都有一个指示其类型的type属性。 Sometimes I have an array of the union type ( Pet[] ) and need to .filter() it based on the type property.有时我有一个联合类型( Pet[] )的数组,需要根据type属性对其进行.filter() That in itself works perfectly fine, but in order to avoid redundant type declarations I want to make sure that the result of the .filter() call is automatically typed properly.这本身工作得很好,但为了避免冗余类型声明,我想确保.filter()调用的结果自动正确输入。

User-defined type guards seemed like the perfect solution for this, so I implemented one that just checks the type property and narrows the type to { type: 'something' } without explicitly declaring the full type (this one's called isCatLike below). 用户定义的类型保护似乎是解决这个问题的完美解决方案,所以我实现了一个只检查type属性并将类型缩小到{ type: 'something' }而不显式声明完整类型的方案(这个在下面称为isCatLike )。 I tried using it inside an if and it correctly narrowed my type from Pet to Cat .我尝试在if中使用它,它正确地将我的类型从Pet缩小到Cat

I then tried to use it as a predicate for .filter() and this time the type wasn't narrowed at all .然后我尝试将它用作.filter()的谓词,这一次类型根本没有缩小 The resulting array was still typed as Pet[] although my if experiment had shown that the type guard was generally able to narrow from Pet to Cat .虽然我的if实验表明类型保护通常能够从Pet缩小到Cat ,但生成的数组仍然被键入为Pet[]

As another experiment I tried to change the type guard slightly and make the type predicate more explicit ( is Cat instead of is { type: 'cat' } and suddenly the .filter() call correctly narrowed the type from Pet[] to Cat[] (this function is called isCat below).作为另一个实验,我尝试稍微更改类型保护并使类型谓词更明确( is Cat而不是is { type: 'cat' }突然.filter()调用正确地将类型Pet[]缩小到Cat[] (下面这个function叫做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 to inspect the types yourself. 打开 TypeScript Playground 上的示例以自行检查类型。

The problem now is that I can't use the more explicit approach (illustrated by the isCat function) because my actual code has a lot more types in the union and there the predicate is also created by a function ( isType(type: string) ).现在的问题是我不能使用更明确的方法(由isCat函数说明),因为我的实际代码在联合中有更多类型,并且谓词也是由 function ( isType(type: string) )。

So what I'm wondering at the moment is this:所以我现在想知道的是:

Why does my "structural type guard" work in the context of an if statement, but not as a predicate for filtering an array?为什么我的“结构类型保护”在if语句的上下文中工作,而不是作为过滤数组的谓词? Shouldn't it work exactly the same way in both cases?在这两种情况下它不应该以完全相同的方式工作吗? Am I doing something wrong or have I hit a limitation of the type system?我做错了什么还是我遇到了类型系统的限制?

This boils down to how the types for Array.filter are written:这归结为如何编写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[];
}

The key bit of that signature is <S extends T> , where in this case, T is Pet and S would be { type: "cat" } .该签名的关键位是<S extends T> ,在这种情况下, TPetS{ type: "cat" } However, { type: "cat" } does not extend Pet , so the signature does not apply, so it falls into the normal filter signature.但是{ type: "cat" }没有扩展Pet ,所以签名不适用,所以属于普通的过滤器签名。


It works in the single element case, because TS's narrowing logic is actually a bit smarter than can be expressed in the signature for .filter - it actually combines the result of the type guard and the original type - something like Pet & { type: "cat" } which is the same as Cat .它适用于单个元素的情况,因为 TS 的缩小逻辑实际上比.filter的签名中表达的要聪明一些——它实际上结合了类型保护的结果和原始类型——比如Pet & { type: "cat" }Cat相同。

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

pet is Fish is our type predicate in this example.在这个例子中,pet is Fish 是我们的类型谓词。 A predicate takes the form parameterName is Type, where parameterName must be the name of a parameter from the current function signature.谓词采用 parameterName is Type 的形式,其中 parameterName 必须是当前 function 签名中的参数名称。 Any time isFish is called with some variable, TypeScript will narrow that variable to that specific type if the original type is compatible .任何时候使用某个变量调用 isFish 时,如果原始类型兼容,TypeScript 就会将该变量缩小到该特定类型。

I would guess that since the original type is any in the type guard, TS can't narrow it down at all, if you swap any for Cat you even get a compilation error saying that {type: 'cat'} is not assignable to Cat cause of missing the other two props.我猜想由于类型保护中的原始类型是any ,TS 根本无法缩小范围,如果你将any换成Cat你甚至会得到一个编译错误,说{type: 'cat'}不可分配给Cat缺少其他两个道具的原因。

I took that as a hint and made those other props optional: type Cat = { type: 'cat'; name?: string; purrs?: boolean }我把它当作一个提示,并让其他道具可选: type Cat = { type: 'cat'; name?: string; purrs?: boolean } type Cat = { type: 'cat'; name?: string; purrs?: boolean }

Your predicate starts working as you want it to then.然后,您的谓词开始按您的意愿工作。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM