我有一个number[] | string[]
,我想过滤它。问题是过滤函数将类型改为(number | string)[]
。
// list of numbers OR list of strings
let myList = Math.random() < 0.5 ? [1, 2, 3] : ["a", "b", "c"];
// filter the list
const filtered = myList.filter((x) => { return true; });
// oh no, the result is Array<number | string> instead of (number[] | string[])
myList = filtered;
最后一行错误:
Type '(string | number)[]' is not assignable to type 'number[] | string[]'.
Type '(string | number)[]' is not assignable to type 'number[]'.
Type 'string | number' is not assignable to type 'number'.
Type 'string' is not assignable to type 'number'.(2322)
我该如何表达这个事实:数组不能混合,但它可以包含任一类型的元素?
TypeScript 不会自动将泛型函数/方法调用的返回类型分布在其参数的联合中。从概念上讲,当使用类型的参数调用形式为 的函数时,应该产生类型为 的输出。但是现在这并没有发生。这样的函数调用要么被彻底拒绝,要么被接受但产生类型为 的输出。在microsoft/TypeScript#52295有一个功能请求,要求自动将函数应用分布在联合类型参数上,但这不是一个非常受欢迎的功能,实现它会显著降低编译器性能。因此,除非明确编写泛型函数以在其输入中分布在联合中,否则它不会发生。
<T>(i: I<T>)=>O<T>
I<T1> | I<T2> | I<T3>
O<T1> | O<T2> | O<T3>
O<T1 | T2 | T3>
TypeScript库的类型如下
Array.prototype.filter()
所示:这是一个具有多个调用签名的重载
predicate
方法。第一个调用签名处理充当类型保护函数的情况,并允许输出数组类型比输入 更窄(因此["a", "b", 1, 2, "c"].filter(x => typeof x === "string")
是 类型string[]
而不是(string | number)[]
)。第二个调用签名是通常的情况,其中谓词不能用于缩小元素类型。这两个签名都不会尝试在调用该方法的数组中分配并集。如果您想要这样的行为,则需要自己明确编写。
如果您希望代码库中的所有数组都自动发生联合分布行为,则可以将新的重载调用签名合并
Array<T>
到全局接口中(也许ReadonlyArray<T>
也可以)。 也许是这样的:A
这里我们为方法添加了一个新的泛型类型参数filter()
,表示调用该方法的数组的类型。为了正确推断该类型,该方法具有类型为 的this
参数A
。this
参数是运行时不存在的假装参数;它只是表示调用该方法的对象的类型。因此,当您调用 时myList.filter(⋯)
,应该将其A
推断为typeof myList
。然后有很多类型的处理。类型
A extends Array<infer T> ? ⋯ : never;
是分布式条件类型⋯
,当A
是联合输入时,它会明确生成输出的联合。对于类型保护的情况,我们必须尝试将每个联合成员从其原始元素类型的数组缩小到也可以分配给类型保护类型的数组S
,因此是交集。对于正常情况,我们仍然必须分布在常规元素类型上。您可能想知道为什么我们不直接返回A
。那是因为A
可能是像这样的元组类型[string, number, boolean]
,而我们不想返回filter()
相同的类型。像这样的类型(string | number | boolean)[]
更合适。这会导致以下行为:
效果很好,但可能会出现一些我没有预料到的奇怪极端情况。如果你不想将其应用于整个代码库,你可能只想编写一个
filter()
使用这些调用签名作为包装器的函数:这有点简单,因为我们不必处理
this
参数和推断。TypeScript 可以A
直接从实际arr
参数推断。行为是相同的:但是现在您不必担心本机
filter()
数组方法在代码库的某些远程部分会出现奇怪的边缘情况行为。但即便如此,这也有些复杂,并且仍然可能存在极端情况。如果你要对数组的并集进行大量过滤,那么我认为这是值得的。但如果你只打算在代码库中执行一两次,那么你可能应该只使用类型断言并继续:
这是迄今为止最简单的方法,虽然它不是可验证的类型安全的,但其他方法的复杂性可能更容易出错。我的意思是,一方面你告诉 TypeScript 这
filtered0
是一个string[] | number[]
,另一方面你试图告诉 TypeScript 每次调用 都会filter()
在类型级别使用联合执行这个复杂的操作。如果你不太确定这个复杂的操作是否正确,那么验证你的断言可能更安全。游乐场链接到代码
不幸的是,一些标准 JS/DOM 方法定义的类型比预期的要广泛,您可以
Array
像这样进行扩充:操场