问题
I am looking for a way to have union types as function arguments, then be able to use the arguments, any missing arguments would be undefined
However here, name and age are causing a type issue.
function example(props: { id: number } & ({ name: string } | { age: number })) {
const { id, name, age } = props
}
This is what I'd like:
example({ id: 1, name: "Tom" })
example({ id: 1, age: 31 })
回答1:
A small variation of StrictUnion
found here will work well:
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
The way StrictUnion
works is by ensuring all constituents of the union have all members from all the constituents of the union. It ensures this by adding any members that are missing with the type undefined
. So this type: { id: number } & ({ name: string } | { age: number })
will become this type: { id: number; name: string; age: undefined } | { id: number; name: undefined; age: number }
. Since this new type has the same structure we can de-structure it.
To build StrictUnion
we must first get a union of all the keys from all union constituents. To do this we must use the distributive behavior of conditional types. Using this behavior we can build a type that extracts extracts the keys of each union constituent and creates a union of all. To trigger the distributive behavior we can use an always true condition ( T extends T
, T extends unknown
or, less ideal T extends any
). With this we arrive at the following type extract all the keys:
type UnionKeys<T> = T extends T ? keyof T : never;
Below we can see how this type is applied:
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"
After we have UnionKeys
we can use another distributive conditional type to go through each of the union members and see what keys are missing from a given type T
(using Exclude<UnionKeys<TAll>, keyof T>
) and intersecting the original T
with a Partial
Record
that contains these keys typed as undefined
. We need to pass the union to the distributive type twice, once to distribute over (T
), and once to have the whole union to be able to extract the keys using UnionKeys
.
Below we ca see how this type is applied:
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 }
回答2:
One possible solution is to default the "undefinable" arguments.
function example(props: { id: number } & ({ name: string } | { age: number })) {
const available = { ...{ name: undefined, age: undefined }, ...props }
}
来源:https://stackoverflow.com/questions/59601795/checking-for-union-type