使用 Array.every 缩小数组类型的并集

Narrowing a union of array types using Array.every

我有一个类型为不同数组类型联合的变量。我想将其类型缩小为该联合的单个成员,这样我就可以 运行 为每种类型在其上添加适当的代码。在这里使用 Array.every and a custom type guard 似乎是正确的方法,但是当我尝试使用此 TypeScript 时会抱怨“此表达式不可调用”。连同我不明白的解释。

这是我的最小可重现示例:

const isNumber = (val: unknown): val is number => typeof val === 'number';

const unionArr: string[] | number[] = Math.random() > 0.5 ? [1, 2, 3, 4, 5] : ['1', '2', '3', '4', '5'];

if (unionArr.every(isNumber)) { // <- Error
  unionArr;
}

TypeScript Playground

这是错误:

This expression is not callable.
  Each member of the union type
    '{
      <S extends string>(predicate: (value: string, index: number, array: string[]) => value is S, thisArg?: any): this is S[];
      (predicate: (value: string, index: number, array: string[]) => unknown, thisArg?: any): boolean;
    } | {
      ...;
    }'
  has signatures, but none of those signatures are compatible with each other.

这并不妨碍我继续。我发现在我缩小其类型之前使用 type assertion 将我的数组重新转换为 unknown[] ,就像我编写 isNumberArray 自定义类型保护时我会做的那样,无需删除错误危及类型安全。

我还发现将我的 string[] | number[] 数组重铸为 (string | number)[] 可以消除错误。

然而,数组的类型似乎没有正确缩小,所以我需要在检查后使用额外的 as number[]

const isNumber = (val: unknown): val is number => typeof val === 'number';

const unionArr: string[] | number[] = Math.random() > 0.5 ? [1, 2, 3, 4, 5] : ['1', '2', '3', '4', '5'];

if ((unionArr as unknown[]).every(isNumber)) { // <- No error
  unionArr; // <- Incorrectly typed as string[] | number[]
}

if ((unionArr as (string | number)[]).every(isNumber)) { // <- No error
  unionArr; // <- Incrrectly typed as string[] | number[]
}

TypeScript Playground

我也尝试与非数组联合进行比较,当然在这种情况下我只是直接使用自定义类型保护而不是将其与 Array.every 一起使用。在那种情况下,也没有错误并且正确缩小了类型:

const isNumber = (val: unknown): val is number => typeof val === 'number';

const union: string | number = Math.random() > 0.5 ? 1 : '1';

if (isNumber(union)) {
  union; // <- Correctly typed as number
}

TypeScript Playground

因为我有安全类型断言解决方法,所以我可以继续而不需要理解它。但我仍然很困惑为什么首先会出现该错误,因为我正在尝试将类型联合缩小为该联合的单个成员。

我猜这与 TypeScript 键入 Array.every 的方式有关,除了我已经在使用的解决方法外,我可能无能为力。但是当我真的不明白哪里出了问题时,很难确定这一点。我可以在这里做些不同的事情,还是 as unknown[] 类型断言我使用了正确或最好的方法来处理这个问题?

让我们举一个更简单的例子:

type StringOrNumberFn = (a: string | number) => void
type NumberOrBoolFn = (a: number | boolean) => void
declare const justNumber: StringOrNumberFn | NumberOrBoolFn

// works
justNumber(123) 

// errors
justNumber('asd')
justNumber(true)

当您尝试调用函数联合时,实际上调用的是这些成员的 交集 的函数。如果您不知道它是哪个函数,那么您只能以 both 函数支持的方式调用该函数。在这种情况下,两个函数都可以采用 number,所以这是允许的。

如果这些函数的交集有不兼容的参数,则无法调用该函数。那么让我们建模:

type StringFn = (a: string) => void
type NumberFn = (a: number) => void
declare const fnUnion: StringFn | NumberFn

fnUnion(123)    // Argument of type 'number' is not assignable to parameter of type 'never'.(2345)
fnUnion('asdf') // Argument of type 'string' is not assignable to parameter of type 'never'.(2345)

这更接近你的问题。

Playground


字符串数组的 every 和数字数组的 every 被键入以接收不同的参数。

(value: string, index: number, array: string[]) // string[] every() args
(value: number, index: number, array: number[]) // number[] every() args

这与上面的问题本质上是一样的。


所以我认为编译器无法在该联合上调用 every 方法。

相反,我可能会为整个数组编写一个类型断言并手动遍历它。

const isNumberArray = (array: unknown[]): array is number[] => {
  for (const value of array) {
    if (typeof value !== 'number') return false
  }

  return true
}

declare const unionArr: string[] | number[]

if (isNumberArray(unionArr)) {
  Math.round(unionArr[0]); // works
}

Playground