1

TypeScript Playground 链接

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

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
4

1 回答 1

0

两者objobj.$case都是联合类型,但编译器无法遵循它们之间的相关性以便按obj[obj.$case]原样接受等。例如,编译器没有意识到,虽然objcan be a{ $case: 'first'; first: FirstType }obj.$casecan be "second",但它们不能同时为真:

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

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

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提交了关于相关联合类型普遍缺乏语言支持的信息……目前(以及可预见的未来),您需要解决它。


如果您知道编译器不了解类型的某些内容,则可以使用类型断言来告诉编译器您知道什么。这将确保类型安全的负担转移到您身上,因此您需要注意您告诉编译器的内容实际上是正确的。

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

以下是编写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;
}

调用签名表示使用键(类型)索引到obj(类型)的操作,如果它实际上是一个键(所以 type ),或者如果它不是。T$caseKT[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,一个数字文字类型,它比 更具体number。那是因为myObj是用constassertion 声明的。无论如何,编译器肯定知道这res是 a number,正如它愿意允许 所表明的那样res.toFixed()

Playground 代码链接

于 2021-05-13T02:42:32.037 回答