【问题标题】:Helper function to un-discriminate a discriminated union帮助功能取消歧视的工会
【发布时间】:2021-05-13 10:42:32
【问题描述】:

TypeScript Playground Link

我正在使用 protobuf oneof union types generated by ts-proto,并且我想构建一个可以从联合中提取分支的辅助函数。但是,我无法设计一组类型约束来表示我的助手的返回类型应该等于联合分支中相应属性的类型。我怎样才能做到这一点?

type FirstType = number;
type SecondType = string;
type ThirdType = number[];

type MyUnion  =
      { $case: 'first'; first: FirstType }
    | { $case: 'second'; second: SecondType }
    | { $case: 'third'; third: ThirdType };

type UnionCase = MyUnion['$case'];
// This doesn't work: K can't be used in the computed property
type UnionBranch<K extends UnionCase, T> = T extends {$case: K, [K]: infer Prop} ? Prop : never;

function getByCase<K extends UnionCase, T>(obj: MyUnion, $case: K): T | undefined {
  // If I were comparing against a literal, the type would be narrowed within this branch.
  if (obj.$case === $case) {
    // It can't realize that $case has the same name as the property
    return obj[$case];
  }
  return undefined;
}

const myObj = {
    $case: 'first',
    first: 5,
} as const;

const res = getByCase(myObj, 'first');
// res should be type: number

【问题讨论】:

    标签: typescript


    【解决方案1】:

    objobj.$case 都属于 union 类型,但编译器无法遵循它们之间的相关性,以便将 obj[obj.$case] 或类似的东西接受为-是。例如,编译器没有意识到,虽然obj 可以是{ $case: 'first'; first: FirstType }obj.$case 可以是"second",但它们不能同时为真:

    function oops(obj: MyUnion) {
      return obj[obj.$case]; // error!
      // expression of type '"first" | "second" | "third"' 
      // can't be used to index type 'MyUnion'.
    }
    

    目前让编译器在这里验证类型安全的唯一方法是使用control flow analysisobj 依次缩小到每个联合成员,并让编译器评估每个缩小的表达式。这是类型安全的,但冗余重复和冗余:

    function okay(obj: MyUnion) {
      switch (obj.$case) {
        case "first": return obj[obj.$case] // okay
        case "second": return obj[obj.$case] // okay
        case "third": return obj[obj.$case] // okay
      }
    }
    

    我向microsoft/TypeScript#30581 提交了关于相关联合类型普遍缺乏语言支持的问题...目前(以及在可预见的未来),您需要解决它。


    如果您知道编译器不知道的类型,您可以使用type assertions 告诉编译器您知道什么。这将确保类型安全的负担转移到您身上,因此您需要注意您告诉编译器的内容是否真实。

    有时,编写带有单个调用签名的overloaded function 而不是类型断言更容易。重载实现比常规函数实现检查更松散,因此这与类型断言具有相似的效果。

    以下是编写getByCase() 函数的方法:

    // call signature
    function getByCase<T extends MyUnion, K extends UnionCase>(
      obj: T, $case: K
    ): K extends keyof T ? T[K] : undefined;
    
    // implementation 
    function getByCase(obj: any, $case: UnionCase) {
      if (obj.$case === $case) {
        return (obj[$case]);
      }
      return undefined;
    }
    

    调用签名表示使用键$case(类型K)索引到obj(类型T)的操作,如果它实际上是一个键(所以类型T[K]),或者undefined 如果不是。

    实现非常松散,使用any 表示obj 的类型。你可以试着把东西弄得更紧一些,但你很容易与相关的工会问题发生冲突,我不知道这是否值得。只需对实现进行三重检查,以确保您做的是正确的事情并且没有打错字(例如,if (obj.$case !== $case))。


    让我们看看它是否有效:

    const myObj = {
      $case: 'first',
      first: 5,
    } as const;
    
    const res = getByCase(myObj, 'first'); // 5
    console.log(res.toFixed(2)) // "5.00"
    

    看起来不错。推断res 具有5 的类型,一个数字literal type,它比number 更具体。那是因为myObj 是用const assertion 声明的。无论如何,编译器肯定知道res 是一个number,正如它愿意允许res.toFixed() 所表明的那样。

    Playground link to code

    【讨论】:

      猜你喜欢
      • 2011-03-12
      • 2019-05-04
      • 2011-02-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多