使用 typegaurd 过滤数组不会生成该类型的数组

Filter an array with typegaurd doesn't produce an array of that type

我在将联合类型的数组过滤为单一类型时遇到问题。

我的代码与此示例非常相似:

interface Section {
  type: 'section';
  name: string;
  children: (Section | Link)[];
}

interface Link {
  type: 'link';
  title: string;
  children: []
}

const json = JSON.stringify({
  type: 'section',
  name: 'Parent',
  children: [
    { type: 'section', name: 's1', children: [] },
    { type: 'section', name: 's2', children: [{ type: 'section', name: 's2', children: [] }] },
    { type: 'link', title: 'l1', children: [] },
    { type: 'section', name: 's3', children: [] },
    { type: 'link', title: 'l2', children: [] },
  ]
});

let parentSection: Section | Link = JSON.parse(json);

const sections = parentSection.children.filter((item): item is Section => !!item && item.type === 'section');

TypeScript Playground

JSON.stringifyJSON.parse 在那里,因此 TS 不会从数据中推断出 parentSection 的类型,该数据来自现实世界中的 API。

问题是 TS 推断的 sections 的类型是 (Section | Link)[],但我希望它是 Section[]Section[] | []

我需要在此处更改什么才能使它像我 expect/want 一样正常工作?我不能影响数据本身,但我是为它编写类型的人,所以我改变了对这些类型的看法。 Link 项始终随 children 数组一起提供,其中没有任何项。

非常感谢您的帮助。谢谢!

这里的问题是你的值为 union type (Section | Link)[] | [] and you're trying to call its filter() method, which is therefore also a union of function types. Taking a union of functions with differing call signatures and deciding which calls are safe is not a trivial task. Conceptually a union of call signatures should take an intersection of the parameters (due to contravariance of function/method parameters),但有很多实际问题使它变得更难。

本来TypeScript就直接放弃了,根本不让你调用这样的函数。请参阅 microsoft/TypeScript#7294. Then TypeScript 3.3 introduced support for calling some union types via a fix in microsoft/TypeScript#29011, which would synthesize a single call signature using the intersections of the parameters from the individual union members' call signatures, as long as no more than one member's call signature is overloaded and no more than one member's call signature is a generic. This improved things a lot, but in particular array methods tend to be generic and/or overloaded so unions of arrays still had few useful methods available. See microsoft/TypeScript#36390. A little more work has been done in microsoft/TypeScript#31023 以允许在联合中调用一些泛型方法,截至目前,只有 reduce() 对于函数的联合是完全不可调用的。尽管如此,它并不完美,而且 microsoft/TypeScript#44373 is the currently open issue about this. Your filter() call is specifically trying to use the overloads where the callback is a user-defined type guard function,只是目前不支持。


因此目前只有解决方法。我的首选解决方法:

一般来说,当您有一个 Array<A> | Array<B> 类型的数组并且您只打算从中读取数据时,将其视为 ReadonlyArray<A | B> 类型的值应该是安全的。事实上,编译器一般会让你执行这个加宽:

parentSection.children; //  (Section | Link)[] | []
const pc: readonly (Section | Link)[] = parentSection.children; // no error

请注意,[] 的元素类型是 the never type,因此在这种情况下 ReadonlyArray<A | B> 将是 ReadonlyArray<(Section | Link) | never>,即 ReadonlyArray<Section | Link>

一旦你有了 ReadonlyArray<A | B>,它就不再是联合类型(它的元素是联合类型但不是数组本身)你可以调用 filter() 而不受惩罚:

const sections = pc.filter((item): item is Section => !!item && item.type === 'section');
// const sections: Section[]

请注意,您可以将其缩短为一行:

const sections = (parentSection.children as readonly (Section | Link)[]).
  filter((item): item is Section => !!item && item.type === 'section');
// const sections: Section[]

但这有点不安全,因为 type assertions allow (unsafe) narrowing/downcasting as well as (safe) widening/upcasting, whereas type annotations 变量只允许 widening/upcasting.


当然还有其他可行的解决方法(例如,您提到使用 map(x => x),由于上述一些 PR,它可以工作,然后它变成一个单一的数组类型)。除非 microsoft/TypeScript#44373 得到解决,否则您必须坚持使用您最喜欢的解决方法。

Playground link to code