更好的 TypeScript union-narrowing type guard 函数人体工程学

Better egonomics for TypeScript union-narrowing typeguard function

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

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;
}

它可以在 filter 调用中轻松使用,以将数组缩小到特定的联合成员,但是它要求您记得在字符串文字 [=15= 之后添加 as const ] 参数,否则它会错误地推断类型并给你一个太宽的结果:

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));

虽然可以使用预制常量部分缓解此问题,

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));

它仍然不能阻止人们忘记 as const 的字面值并混淆为什么事情不起作用的问题(以及 as const 只是看起来丑陋的事实上面的示例两个代码块)。

有没有办法让 TS 在 narrow 中提供字符串文字时自动推断更窄的类型,如上所示?或者至少是一种显示错误消息的方法?

Playground link for all of the above code.

您只需要对 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[]

Playground

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

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

您可能已经注意到,我将所有通用名称都大写了,因为我觉得很难读。不过这是个人喜好问题。