表格有a call signature for the ReadonlyArray filter() method in the TypeScript standard library
interface Array<T> {
filter<S extends T>(
predicate: (value: T, index: number, array: readonly T[]) => value is S,
thisArg?: any
): S[];
}
这意味着如果你传入一个编译器理解为user-defined type guard function; i.e., one that returns a type predicate 的predicate 回调,那么filter() 的返回类型可以是比原始数组更窄的数组类型。
这是我能想到的唯一可行的方法。
一个问题是编译器将无法推断i => typeof i.value === "string" 是这样一个类型保护函数。或者,更一般地说,编译器不会从任何函数实现的主体中推断类型谓词返回类型。如果你想让一个函数成为一个类型保护函数,你需要手动注释它。
microsoft/TypeScript#38390 有一个功能请求,要求对类型保护函数的自动推断提供一些支持,也许如果实现了,那么i => typeof i.value === "string" 会神奇地做你想做的事。但是现在无论如何我们都需要放弃它。
一种可能的方法如下所示:
const hasStringValue = <T extends { value: any }>(
i: T): i is Extract<T, { value: string }> => typeof i.value === "string"
函数hasStringValue 是一个generic 类型保护函数,它接受T constrained 类型的输入constrained 到{value: any},并返回一个boolean 值,该值可用于缩小范围如果true,则i 输入Extract<T, {value: string}>。我使用the Extract utility type 是因为我们希望T 是union 类型,其中一些可以分配给{value: string}。
让我们试试吧:
/* const arr: readonly [{
readonly id: 1;
readonly value: "foobar";
}, {
readonly id: 2;
readonly value: () => string;
}] */
const reduced = arr.filter(hasStringValue);
/* const reduced: {
readonly id: 1;
readonly value: "foobar";
}[] */
console.log(
reduced.map(i => i.value.toUpperCase())
) // ["FOOBAR"]
看起来不错。原来的数组arr有一个元素类型的union,filter()的输出是一个新的数组reduced,它的元素类型只有id: 1对应的那个。
因为我们是“手工”完成的,所以有一些注意事项。首先,编译器不知道如何检查您是否正确实现了用户定义的类型保护功能;见microsoft/TypeScript#29980。所以没有什么能阻止你这样做:
const hasStringValue = <T extends { value: any }>(
i: T): i is Extract<T, { value: string }> => typeof i.value !== "string"
// oops ----------------------------------------------------> ~~~
如果编译器没有警告你,这会在运行时导致坏事:
const reduced = arr.filter(hasStringValue);
console.log(reduced.map(i => i.value.toUpperCase())) // no compiler warning, but
// ? RUNTIME ERROR! i.value.toUpperCase is not a function
这只能通过小心来解决。
其次,您的数组元素也可能属于T 类型,其中Extract<T, {value: any}> 保留或消除了您不希望的类型。例如,value 可能比字符串宽:
const hmm = [{ value: Math.random() < 0.5 ? "hello" : 123 }];
hmm.filter(hasStringValue) // never[]
糟糕,Extract<{value: string | number}, {value: string}> 是 never,因为左侧不能分配给右侧。
这可以通过更改 hasStringValue 来做更复杂的事情来解决,例如检查潜在的重叠而不是子类型。我不打算在这里切线这样做,特别是因为其他T 类型可能有其他极端情况需要担心。关键是您需要弄清楚哪种类型的谓词实际上适用于您的输入类型,因此您必须进行测试。
所以,呃,小心点。
Playground link to code