检查联合类型

Checking for union type

我正在寻找一种将联合类型作为函数参数的方法,然后能够使用这些参数,任何缺少的参数都将是未定义的

但是这里,姓名和年龄导致了类型问题。

function example(props: { id: number } & ({ name: string } | { age: number })) { 
  const { id, name, age } = props
}

这就是我想要的:

example({ id: 1, name: "Tom" })

example({ id: 1, age: 31 })

一种可能的解决方案是默认 "undefinable" 参数。

function example(props: { id: number } & ({ name: string } | { age: number })) { 
  const available = { ...{ name: undefined, age: undefined }, ...props }

}

StrictUnion 的一个小变体 会很好用:

type UnionKeys<T> = T extends T? keyof T : never;
type StrictUnionHelper<T, TAll> = T extends T? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, undefined>> : never;
type StrictUnion<T> = StrictUnionHelper<T, T>

function example(props: StrictUnion<{ id: number } & ({ name: string } | { age: number })>) { 
  const { id, name, age } = props
}

Playground Link

StrictUnion 的工作方式是确保联盟的所有成员都拥有来自联盟所有成员的所有成员。它通过添加类型 undefined 缺少的任何成员来确保这一点。所以这种类型:{ id: number } & ({ name: string } | { age: number }) 将变成这种类型:{ id: number; name: string; age: undefined } | { id: number; name: undefined; age: number }。由于这种新类型具有相同的结构,我们可以对其进行解构。

要构建 StrictUnion,我们必须首先从所有并集成分中获取所有键的并集。为此,我们必须使用 distributive behavior 条件类型。使用这种行为,我们可以构建一个类型,提取每个联合成分的键并创建所有的联合。要触发分配行为,我们可以使用始终为真的条件(T extends TT extends unknown 或不太理想的 T extends any)。有了这个,我们得到以下类型提取所有键:

type UnionKeys<T> = T extends T ? keyof T : never;

下面我们可以看到这种类型是如何应用的:

type A = { id: number; name: string }
type B = { id: number; age: number }

UnionKeys<A | B>
  // Conditional type is applied to A and B and the result unioned
  <=> (A extends unknown ? keyof A: never) | (B extends unknown ? keyof B: never) 
  <=> keyof A | keyof B
  <=> ("id" | "name") | ("id" | "age")
  <=> "id" | "name" | "age"

在我们有了 UnionKeys 之后,我们可以使用另一种分布式条件类型来遍历每个联合成员,并查看给定类型 T 中缺少哪些键(使用 Exclude<UnionKeys<TAll>, keyof T> ) 并将原始 TPartial Record 相交,其中包含这些键入为 undefined 的键。我们需要两次将 union 传递给分布式类型,一次是分配给 (T),一次是让整个 union 能够使用 UnionKeys.

提取键

下面我们可以看到这种类型是如何应用的:

type A = { id: number; name: string }
type B = { id: number; age: number }
StrictUnion<A | B>
  <=> StrictUnionHelper <A | B, A | B>
  // Distributes over T
  <=> (A extends A ? A & Partial<Record<Exclude<UnionKeys<A | B>, keyof A>, undefined>> : never) | (B extends B ? B & Partial<Record<Exclude<UnionKeys<A | B>, keyof B>, undefined>> : never)
  <=> (A extends A ? A & Partial<Record<Exclude<"id" | "name" | "age", "id" | "name">, undefined>> : never) | (B extends B ? B & Partial<Record<Exclude<"id" | "name" | "age", "id" | "age">, undefined>> : never)
  <=> (A extends A ? A & Partial<Record<"age", undefined>> : never) | (B extends B ? B & Partial < Record < "name" >, undefined >> : never)
  // The condition A extends A and B extends B are true and thus the conditional type can be decided
  <=> (A & Partial<Record<"age", undefined>>) | (B & Partial<Record<"name">, undefined>>)
  <=> { id: number; name: string; age?: undefined } | { id: number; age: number; name?: undefined }