【问题标题】:Getting strong types back from a filter using Typescript使用 Typescript 从过滤器中获取强类型
【发布时间】:2021-10-24 05:52:35
【问题描述】:

如果有人找到任何可以生成强类型 map/reduce 的好类型实用程序,我很感兴趣。例如,如果我有以下数组:

const arr = [
  { id: 1, value: "foobar" },
  { id: 2, value: () => "hello world" }
] as const;

假设我想要以下内容:

  • reduce 到那些在 value 属性中具有字符串的元素
  • 在返回数组上保留强类型

对于运行时,这是非常简单地使用内置在数组原型中的内置filter()

const reduced = arr.filter(i => typeof i.value === "string");

但是由此返回的type与之前没有变化。我想要的是让类型系统也响应缩减的数组。为了做到这一点,我有一个函数isString,它在运行时测试给定的输入是否是字符串,但也返回布尔文字,以便类型系统可以推断出返回的真/假值。

作为参考,isString 实用程序如下所示:

type IsString<T> = T extends string ? true : false;

function isString<T>(i: T) {
  return (typeof i === "string") as IsString<T>;
}

此实用程序适用于类型和运行时系统,因此天真地希望以下内容可以工作:

const reduced2 = arr.filter(i => isString(i));

不幸的是,这仍然只是返回相同的联合类型。如何让它发挥作用?

注意:我也想做其他 map-reduce 功能,但过滤操作对我来说是最紧迫的。

Playground

【问题讨论】:

    标签: typescript typescript-typings typescript-generics


    【解决方案1】:

    使用来自 TypeScript 库的 Array#filter 调用签名...

    interface Array<T> {
      filter<S extends T>(
        predicate: (value: T, index: number, array: readonly T[]) => value is S, 
        thisArg?: any
      ): S[];
    }
    

    ...您可以像这样定义谓词的返回类型以仅提取所需的类型。

    const arr = [
      { id: 1, value: 'foobar' },
      { id: 2, value: () => 'hello world' },
    ] as const;
    const reduced = arr.filter(
      (item): item is Extract<typeof arr[number], { value: string }> =>
        typeof item.value === 'string'
    ); // has type { readonly id: 1; readonly value: 'foobar' }[]
    

    注意typeof arr[number] 类型;它的类型是{ id: 1, value: 'foobar' } | { id: 2, value: () =&gt; string }

    【讨论】:

      【解决方案2】:

      表格有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 predicatepredicate 回调,那么filter() 的返回类型可以是比原始数组更窄的数组类型。

      这是我能想到的唯一可行的方法。


      一个问题是编译器将无法推断i =&gt; typeof i.value === "string" 是这样一个类型保护函数。或者,更一般地说,编译器不会从任何函数实现的主体中推断类型谓词返回类型。如果你想让一个函数成为一个类型保护函数,你需要手动注释它。

      microsoft/TypeScript#38390 有一个功能请求,要求对类型保护函数的自动推断提供一些支持,也许如果实现了,那么i =&gt; 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&lt;T, {value: string}&gt;。我使用the Extract utility type 是因为我们希望Tunion 类型,其中一些可以分配给{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&lt;T, {value: any}&gt; 保留或消除了您不希望的类型。例如,value 可能比字符串

      const hmm = [{ value: Math.random() < 0.5 ? "hello" : 123 }];
      hmm.filter(hasStringValue) // never[]
      

      糟糕,Extract&lt;{value: string | number}, {value: string}&gt;never,因为左侧不能分配给右侧。

      这可以通过更改 hasStringValue 来做更复杂的事情来解决,例如检查潜在的重叠而不是子类型。我不打算在这里切线这样做,特别是因为其他T 类型可能有其他极端情况需要担心。关键是您需要弄清楚哪种类型的谓词实际上适用于您的输入类型,因此您必须进行测试。

      所以,呃,小心点。

      Playground link to code

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2021-03-01
        • 2014-06-21
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-11-16
        • 2013-07-17
        相关资源
        最近更新 更多