【问题标题】:Narrowing a union of array types using Array.every使用 Array.every 缩小数组类型的联合
【发布时间】:2022-02-07 10:29:13
【问题描述】:

我有一个变量,它的类型是不同数组类型的联合。我想将其类型缩小到该联合的单个成员,因此我可以为每种类型在其上运行适当的代码。在这里使用 Array.everycustom 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[] 类型断言我使用了正确或最好的方法来处理这个问题?

【问题讨论】:

    标签: arrays typescript narrowing


    【解决方案1】:

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

    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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2022-10-02
      • 2021-06-09
      • 2021-10-21
      • 1970-01-01
      • 2021-02-26
      • 2019-10-12
      • 1970-01-01
      • 2021-12-14
      相关资源
      最近更新 更多