【问题标题】:Narrow return type of find from discriminated union array从可区分联合数组中查找的窄返回类型
【发布时间】:2020-03-21 19:42:50
【问题描述】:

我经常使用下面示例中的代码,想知道是否有一些聪明的方法可以键入 find 结果而无需进行显式类型断言。

type Foo = { type: "Foo" };
type Goo = { type: "Goo" };
type Union = Foo | Goo;

const arr: Union[] = [];
const foo = arr.find(a => a.type === "Foo") as Foo;

如果省略as Foo 类型断言,则结果为Union 类型,即使它只能返回Foo 类型。

在此类示例中,修复 find 类型以返回缩小类型的最简洁方法是什么?

编辑:这个问题也可能适用于filter 和其他类似的方法。

Edit2:建议的类似问题的接受答案 (Way to tell TypeScript compiler Array.prototype.filter removes certain types from an array?) 表明,通过在 find/filter 的谓词中使用类型保护,可以缩小返回值。

这种类型保护函数应该如何缩小任何有区别的联合,例如区分字符串文字总是在type 键下?

【问题讨论】:

  • 不完全,但它有帮助。现在只是想弄清楚如何编写一些通用类型保护函数来缩小像这样的可区分联合,这样我就可以编写arr.find(isTypeFromUnion("Foo"))。
  • 您可以将find 的回调函数定义为类型保护,如下所示:const maybeFoo = arr.find((a): a is Foo => a.type === "Foo"); // Foo | undefined。 TS
  • 这只是将as Foo 移动到a is Foo,这意味着我仍然需要显式编写类型
  • Afaik 目前无法让编译器自动推断类型保护并缩小联合,请参阅 here 和 this issue。

标签: typescript type-conversion discriminated-union


【解决方案1】:

感谢 jcalz the excellent accepted answer。

我为谓词守卫注释了一个略有不同的工厂风格:

function hasProp<K extends PropertyKey, V extends string | number | boolean>(k: K, v: V) {
  
  // All candidate types might have key `K` of any type
  type Candidate = Partial<Record<K, any>> | null | undefined

  // All matching subtypes of T must have key `K` equal value `V`
  type Match<T extends Candidate> = Extract<T, Record<K, V>>

  return <T extends Candidate>(obj: T): obj is Match<T> => (
    obj?.[k] === v
  )
}

【讨论】:

    【解决方案2】:

    公认的答案很好,但很难理解。我带着这样的东西去了。它的可重用性较低,仅适用于一种类型,但更容易阅读。

    function isType<V extends Union['type']>(val: V) {
      return (obj: Union):
          obj is Extract<Union, {type: V}> => obj.type === val;
    }
    
    const found = arr.find(isType('Foo'));
    

    【讨论】:

      【解决方案3】:

      这是上面答案中 jcalz 的代码,用否定和联合进行了扩展。

      export function isDiscriminate<K extends PropertyKey, V extends string | number | boolean>(
          discriminantKey: K, discriminantValue: V | V[]
      ) {
          return <T extends Record<K, any>>(
              obj: T & Record<K, V extends T[K] ? T[K] : V>
          ): obj is Extract<T, Record<K, V>> =>
              Array.isArray(discriminantValue) 
                  ? discriminantValue.some(v => obj[discriminantKey] === v)
                  : obj[discriminantKey] === discriminantValue;
      }
      
      export function isNotDiscriminate<K extends PropertyKey, V extends string | number | boolean>(
          discriminantKey: K, discriminantValue: V | V[]
      ) {
          return <T extends Record<K, any>>(
              obj: T & Record<K, V extends T[K] ? T[K] : V>
          ): obj is Exclude<T, Record<K, V>> =>
              Array.isArray(discriminantValue)
                  ? discriminantValue.some(v => obj[discriminantKey] === v)
                  : obj[discriminantKey] === discriminantValue;
      }
      

      及用法:

      type A = { type: "A" };
      type B = { type: "B" };
      type C = { type: "C" };
      type Union = A | B | C;
      
      const arr: Union[] = [];
      arr.find(isDiscriminate("type", "A")); // A
      arr.find(isDiscriminate("type", ["A", "B"])); // A | B
      arr.find(isNotDiscriminate("type", "A")); // B | C
      arr.find(isNotDiscriminate("type", ["A", "B"])) // C
      

      【讨论】:

        【解决方案4】:

        如果你想要一个用户定义的类型保护函数的生成器,它返回一个类型谓词来区分有区别的联合,它可能看起来像这样:

        function discriminate<K extends PropertyKey, V extends string | number | boolean>(
            discriminantKey: K, discriminantValue: V
        ) {
            return <T extends Record<K, any>>(
                obj: T & Record<K, V extends T[K] ? T[K] : V>
            ): obj is Extract<T, Record<K, V>> =>
                obj[discriminantKey] === discriminantValue;
        }
        

        如果我调用discriminate("type", "Foo"),结果是一个签名类似于&lt;T&gt;(obj: T)=&gt;obj is Extract&lt;T, {type: "Foo"}&gt; 的函数。 (我说它是相似,因为实际的返回值将T 限制为仅使用"type" 作为键和可以分配"Foo" 的值。)让我们看看它是如何工作的:

        const foo = arr.find(discriminate("type", "Foo")); // Foo | undefined 
        const goos = arr.filter(discriminate("type", "Goo"));  // Goo[]
        

        看起来不错。如果您传递不适用的字段/值,会发生以下情况:

        const mistake1 = arr.find(discriminate("hype", "Foo")); // error!
        // ------------------->   ~~~~~~~~~~~~~~~~~~~~~~~~~~~
        // Union is not assignable to Record<"hype", any>.
        const mistake2 = arr.find(discriminate("type", "Hoo")); // error!
        // ------------------->   ~~~~~~~~~~~~~~~~~~~~~~~~~~~
        // Union is not assignable to ((Foo | Goo) & Record<"type", "Hoo">)
        

        好的,希望对您有所帮助;祝你好运!

        Link to code

        【讨论】:

        • 是的,这就像魅力一样。正是我所期望的。你能把它扩展到否定和联合吗? a.type !== "Foo" 和 a.type === "Foo" || a.type === "Goo"
        • 是的,很有可能。否定将使用Exclude 而不是Extract,对于联合,您需要接受V[] 类型的数组/剩余参数并检查数组中的每个元素。
        猜你喜欢
        • 2021-10-05
        • 2018-02-28
        • 2020-02-16
        • 1970-01-01
        • 1970-01-01
        • 2018-04-23
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多