I have a discriminated union, for example:
type Union = { a: "foo", b: string, c: number } | {a: "bar", b: boolean }
I need to derive a type that includes all potential properties, assigned with types that may be found on any member of Union
, even if only defined on some - in my example:
type CollapsedUnion = {
a: "foo" | "bar",
b: string | boolean,
c: number | undefined
}
How can I make a generic that derives such collapsed unions?
I need a generic that supports unions of any size.
Similar behaviour can be achieved as a byproduct by using native Omit
utility, but unfortunately for me it leaves out properties that are not present on every union memeber (does not count them in as undefined
or via ?
).
I found a two way(s)!
EDIT: this is a solution with two separate type parameters. See lower down for a solution with a single union type parameter.
// The source types
type A = { a: "foo", b: string, c: number }
type B = { a: "bar", b: boolean }
// This utility lets T be indexed by any (string) key
type Indexify<T> = T & { [str: string]: undefined; }
// Where the magic happens ✨
type AllFields<T, R> = { [K in keyof (T & R) & string]: Indexify<T | R>[K] }
type Result = AllFields<A, B>
/**
* 🥳
* type Result = {
* a: "foo" | "bar";
* b: string | boolean;
* c: number | undefined;
* }
*/
How it works
AllFields
is a mapped type. The 'key' part of the mapped type
[K in keyof (T & R) & string]
means that K
extends the keys of the union T & R
, which means it will be a union of all the keys that are either in T
or in R
. That's the first step. It ensures that we are making an object with all the required keys.
The & string
is necessary as it specifies that K
also has to be a string. Which is almost always going to be the case anyway, as all object keys in JS are strings (even numbers) – except for symbols, but those are a different kettle of fish anyway.
The type expression
Indexify<T | R>
returns the union type of T
and R
but with string indexes added in. This means that TS won't throw an error if we try to index it by K
even when K
doesn't exist in one of T
or R
.
And finally
Indexify<T | R>[K]
means that we are indexing this union-with-undefineds-for-string-indexes by K
. Which, if K
is a key of either T
, R
, or both, will result in that key's value type(s).
Otherwise, it will fall back to the [string]: undefined
index and result in a value of undefined.
EDIT: solution for a single generic parameter
You specified that you don't actually want this to work for two type parameters, but with an existing union type, regardless of how many members are in the union.
It took blood, sweat and tears but I've got it.
// Magic as far as I'm concerned.
// Taken from https://stackoverflow.com/a/50375286/3229534
type UnionToIntersection<U> =
(U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never
// This utility lets T be indexed by any key
type Indexify<T> = T & { [str: string]: undefined; }
// To make a type where all values are undefined, so that in AllUnionKeys<T>
// TS doesn't remove the keys whose values are incompatible, e.g. string & number
type UndefinedVals<T> = { [K in keyof T]: undefined }
// This returns a union of all keys present across all members of the union T
type AllUnionKeys<T> = keyof UnionToIntersection<UndefinedVals<T>>
// Where the (rest of the) magic happens ✨
type AllFields<T> = { [K in AllUnionKeys<T> & string]: Indexify<T>[K] }
// The source types
type A = { a: "foo", b: string, c: number }
type B = { a: "bar", b: boolean; }
type Union = A | B
type Result = AllFields<Union>
/**
* 🥳
* type Result = {
* a: "foo" | "bar";
* b: string | boolean;
* c: number | undefined;
* }
*/
I got UnionToIntersection
from a brilliant answer by @jcalz. I've tried to understand it but can't. Regardless, we can treat it as a magic box that transforms union types into intersection types. That's all we need to get the result we want.