-
Notifications
You must be signed in to change notification settings - Fork 13.3k
Flatten<O> does not merge union object fields — distributes instead #63361
Description
Acknowledgement
- I acknowledge that issues using this template may be closed without further explanation at the maintainer's discretion.
Comment
Problem
I have a utility type Flatten<O> that merges a union of objects into a single flat object. It works correctly at the root level, but when a field's value is itself a union of objects, it is left as-is instead of being recursively flattened.
Implementation
type AllKeys<U> = U extends unknown ? keyof U : never;
type FlattenArray<A extends Record<string, unknown>[]> =
A extends (infer T extends Record<string, unknown>)[]
? Flatten<T>[]
: never;
type Flatten<O extends Record<string, unknown>> = {
[K in AllKeys<O>]: O extends Record<K, infer V>
? V extends Record<string, unknown>[]
? FlattenArray<V>
: V extends Record<string, unknown>
? Flatten<V>
: V
: never;
};What works
Root-level union is flattened correctly:
type Input =
| { mediatorId: string; brokerId: string; }
| { mediatorId: null; brokerId: string; }
| { mediatorId: string; brokerId: null; }
| { mediatorId: null; brokerId: null; };
type Result = Flatten<Input>;
// Correctly produces:
// { mediatorId: string | null; brokerId: string | null; }What does NOT work
When a nested field's value is itself a union of objects, Flatten is not applied — the union is left as-is:
type Input = {
nested:
| { id: string; value: string; }
| { id: string; value: null; };
};
type Result = Flatten<Input>;
// Expected:
// { nested: { id: string; value: string | null; } }
// Actual:
// { nested: { id: string; value: string; } | { id: string; value: null; } }Root cause (suspected)
When V is inferred as a union of objects via O extends Record<K, infer V>, the check V extends Record<string, unknown> distributes over each union member. So Flatten<V> is called per member — Flatten<Obj1> | Flatten<Obj2> — instead of Flatten<Obj1 | Obj2>, which is the whole point.
What I tried
[V] extends [Record<string, unknown>] ? Flatten<V>— tuple brackets to suppress distributivity on the object check. Did not help because the outerV extends Record<string, unknown>[]already splitV.- Making every check non-distributive with tuple brackets from the start.
[NonNullable<V>] extends [object]— caused primitives likestringto incorrectly match sincestringsatisfiesobjectin TS.
Question
How can I call Flatten<V> with the full union of object types rather than distributing it, while still correctly handling primitives, null, and nested arrays?