Repository navigation
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?
Activity
jcalz commentedon Apr 6, 2026
Unfortunately this isn't the place for such questions. You can ask on Stack Overflow or TS Discord, https://www.typescriptlang.org/community/
Your type doesn't work because
O extends Record<K, infer V>distributes over unions inO, which you rely on for it to "work" at the root level, but this breaks nested things. How do to it correctly depends strongly on your use cases. Maybeworks for you, maybe not? But if you need to discuss this further you should probably find a more appropriate venue. Good luck!