TypeScript 联合缩小类型保护函数具有更好的经济性

JKi*_*ian 5 types discriminated-union typescript typescript-generics

我们的代码库中有一个实用程序可以生成类型保护来缩小可区分联合的范围:

export type ExtractBranchFromUnion<
  UNION,
  DISCRIMINANT extends keyof UNION,
  BRANCH extends UNION[DISCRIMINANT],
> = UNION extends Record<DISCRIMINANT, BRANCH> ? UNION : never;

export function narrow<
  UNION,
  DISCRIMINANT extends keyof UNION,
  BRANCH extends UNION[DISCRIMINANT],
>(
  discriminate: DISCRIMINANT,
  branch: BRANCH,
): (
  item: UNION,
) => item is ExtractBranchFromUnion<UNION, DISCRIMINANT, BRANCH> {
  return (item): item is ExtractBranchFromUnion<UNION, DISCRIMINANT, BRANCH> =>
    item[discriminate] === branch;
}
Run Code Online (Sandbox Code Playgroud)

它可以很容易地在调用中使用,filter以将数组缩小到特定的联合成员,但是它要求您记住as const在字符串文字branch参数之后添加,否则它会错误地推断类型并给出太宽的结果:

type A = {type: 'A'; a: string};
type B = {type: 'B'; b: string};
type SimpleUnion = A | B;
const arr: SimpleUnion[] = [];

// BAD: type is SimpleUnion[]
const badListOfBs = arr.filter(narrow('type', 'B'));

// GOOD: type is B[]
const goodListOfBs = arr.filter(narrow('type', 'B' as const));
Run Code Online (Sandbox Code Playgroud)

虽然这个问题可以通过预制常量得到部分缓解,

const Types = {
  'A': 'A',
  'B': 'B',
} as const;

// OKAY: type is B[] but it requires a premade constant
const okayListOfBs = arr.filter(narrow('type', Types.B));
Run Code Online (Sandbox Code Playgroud)

它仍然不能防止人们忘记as const字面值并困惑为什么事情不起作用(以及as const上面的示例两个代码块中看起来丑陋的事实)。

有没有办法让 TS 在提供narrow如上所示的字符串文字时自动推断出较窄的类型?或者至少有一种方法可以显示一条漂亮的错误消息?

上述所有代码的 Playground 链接。

cap*_*ian 4

您只需要向Branch泛型添加额外的限制:

export type ExtractBranchFromUnion<
  Union,
  Discriminant extends keyof Union,
  Branch extends Union[Discriminant],
  > = Union extends Record<Discriminant, Branch> ? Union : never;

export function narrow<
  Union,
  Discriminant extends keyof Union,
  Branch extends Union[Discriminant] & PropertyKey, // <------ change is here
  >(
    discriminate: Discriminant,
    Branch: Branch,
): (
    item: Union,
  ) => item is ExtractBranchFromUnion<Union, Discriminant, Branch> {
  return (item): item is ExtractBranchFromUnion<Union, Discriminant, Branch> =>
    item[discriminate] === Branch;
}



type A = { type: 'A'; a: string };
type B = { type: 'B'; b: string };

type SimpleUnion = A | B;
const arr: SimpleUnion[] = [];

const badListOfBs = arr.filter(narrow('type', 'B')); // B[]
const goodListOfBs = arr.filter(narrow('type', 'A')); // A[]
Run Code Online (Sandbox Code Playgroud)

操场

我添加了这个:Branch extends Union[Discriminant] & PropertyKey

我愿意打赌鉴别器值应该是string | number | symbol

您可能已经注意到,我将所有通用名称大写,因为我发现它很难阅读。但这是偏好问题。

  • 这是因为“Union”没有任何限制。您还可以像[此处](https://tsplay.dev/WKVKom)一样使用此约束“Union extends Record&lt;string, string&gt;”并删除“&amp; PropertyKey”。这样的约束有助于 TypeScript 缩小文字类型。请参阅我的[文章](https://catchts.com/infer-arguments) (2认同)
  • 太棒了,感谢您的解决方案和解释!效果很好。对于我们的用例,我进行了调整以使用“DiscriminantLiteral”类型而不是“PropertyKey”(定义为“type DiscriminantLiteral = PropertyKey | boolean;”),因为我们偶尔可能会使用“true”/“false”来区分联合成员。 (2认同)