My interpretation of the Flatten<T>
operation is like condensing a union of object types into a single object type, assuming that object types were sealed or "exact" (as mentioned in microsoft/TypeScript#12936) and did not allow extra properties.
Presently, object types are open or extensible, and they permit excess properties. For instance, a value of type {a: string}
may have a string
-valued property a
, along with other properties such as b
. The absence of b
in the type definition does not mean it's missing. The compiler considers the property at key b
in {a: string}
to be of type unknown
, because it could be anything. Therefore, the b
property in {a: string} | {b: number}
is also of type unknown
. This results in flattening to something like {a?: unknown, b?: unknown}
, which is not ideal.
There isn't currently a way to specify that all unspecified keys should be prohibited. You can make use of
Exact<{a: string}> | Exact<{b: number}>
but this does not completely achieve what you desire. If a particular key must be forbidden, the closest solution is to define it as an
optional property with a value of
never
.
The plan here involves collecting all keys from each union member of T
:
- Determine
AllKeys<T>
- keys from all union members of T
.
- Restrict each union member of
T
by prohibiting unmentioned keys from AllKeys<T>
.
- Combine the new union members into a single object type.
For example, if T
is {a: string} | {b: number}
:
AllKeys<T>
would be "a" | "b"
.
- Restrictions on unmentioned keys for each member:
- From
{a: string}
to {a: string; b?: never}
to restrict b
.
- From
{b: number}
to {a?: never; b: number}
to restrict a
.
- Merge
{a: string; b?: never} | {a?: never; b: number}
into a single object type {a?: string; b?: number}
.
This explains the implementation approach.
Let's begin with implementing AllKeys<T>
:
type AllKeys<T> = T extends unknown ? keyof T : never;
This straightforward conditional type gathers keys from each member of T
together.
We then move on to creating ProhibitExtra<T, K>
which prohibits any extra keys from K
in an object type T
:
type ProhibitExtra<T, K extends PropertyKey> =
T & { [P in Exclude<K, keyof T>]?: never };
This operation includes optional never
-typed properties in T
for every unmentioned key in
K</code.</p>
<p>Next, we develop <code>ExactifyUnion<T>
to apply
ProhibitExtra<T, K>
to unions in
T
, where
K
is
AllKeys<T>
for the original
T
:
type ExactifyUnion<T, K extends PropertyKey = AllKeys<T>> =
T extends unknown ? ProhibitExtra<T, K> : never
Finally, we create Flatten<T>
by utilizing ExactifyUnion<T>
to obtain a new union and collapsing it into a single object type:
type Flatten<T> = Omit<ExactifyUnion<T>, never>;
The above method leverages the fact that the Omit
utility type does not distribute over unions, making it an easy way to create a non-distributed mapped type.
Let's test our implementation:
type F1 = Flatten<{ a: string } | { b: number }>
// Type F1 = { a?: string; b?: number }
That aligns with the desired output. It's important to note that when a property is present in all union members, it should not be optional:
type F2 = Flatten<{ a: string } | { a: number }>
// Type F2 { a: string | number }
A glimpse into handling optional/required properties across different union members:
type F3 = Flatten<
{ a?: string, b: number, c: boolean, d?: Date } |
{ c: string, d?: number, e: boolean, f?: Date }>
/* Type F3 = {
a?: string;
b?: number;
c: string | boolean;
d?: number | Date;
e?: boolean;
f?: Date;
} */
The results look promising!
Check out the Playground link for the code