1

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 })
ThomasReggi
  • 55,053
  • 85
  • 237
  • 424
  • You can use technique described [here](https://stackoverflow.com/a/49725198/1113002) and define props as `props: { id: number } & RequireAtLeastOne<{ name: string, age: number }>` – Aleksey L. Jan 05 '20 at 17:59
  • Does this answer your question? [typescript interface require one of two properties to exist](https://stackoverflow.com/questions/40510611/typescript-interface-require-one-of-two-properties-to-exist) – Aleksey L. Jan 05 '20 at 17:59

2 Answers2

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 }
Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357
0

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 }

}
ThomasReggi
  • 55,053
  • 85
  • 237
  • 424