Skip to content

Flatten<O> does not merge union object fields — distributes instead #63361

Closed

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 outer V extends Record<string, unknown>[] already split V.
  • Making every check non-distributive with tuple brackets from the start.
  • [NonNullable<V>] extends [object] — caused primitives like string to incorrectly match since string satisfies object in 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

  1. jcalz commented on Apr 6, 2026

    @jcalz
    Contributor

    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 in O, 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. Maybe

    type MergeObject<T extends object, K extends PropertyKey = T extends unknown ? keyof T : never> =
        { [P in K]: Merge<T extends unknown ? P extends keyof T ? T[P] : never : never> } & {}
    
    type Merge<T> = [T] extends [object] ? [T] extends [any[]] ? Merge<T[number]>[] : MergeObject<T> : T

    works for you, maybe not? But if you need to discuss this further you should probably find a more appropriate venue. Good luck!

  2. locked as resolved and limited conversation to collaborators on Jun 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    QuestionAn issue which isn't directly actionable in code

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions