4

I have a union of types having a property also containing union types:

type Union =
  | {
      name: "a";
      event:
        | { eventName: "a1"; payload: string }
        | { eventName: "a2"; payload: number };
    }
  | {
      name: "b";
      event: { eventName: "b1"; payload: boolean };
    };

I'm trying to flatten it to get a single union type like this:

type Result =
  | { name: "a"; eventName: "a1"; payload: string }
  | { name: "a"; eventName: "a2"; payload: number }
  | { name: "b"; eventName: "b1"; payload: boolean };

I tried to use mapped typed and various utility types but couldn't figure it out. Is it something that can actually be done?

1
  • I assume you mean with mapped types, utility types and such. It might be possible, but I feel like it would be impossible to encrypt Commented Feb 9, 2021 at 10:39

2 Answers 2

1

Link to typescript playground

The utility type converting a union to an intersection was fundamental: I've taken it from here

type Union =
    | {
        name: "a";
        event:
        | { eventName: "a1"; payload: string }
        | { eventName: "a2"; payload: number };
    }
    | {
        name: "b";
        event: { eventName: "b1"; payload: boolean };
    };

type nested = {
    [n in Union['name']]: {
        [e in Extract<Union, { name: n }>['event']['eventName']]: {
            name: n,
            eventName: e,
            payload: Extract<Union['event'], {eventName: e}>['payload']
        }
    }
}
// https://stackoverflow.com/a/50375286/3370341
type UnionToIntersection<U> = 
  (U extends any ? (k: U)=>void : never) extends ((k: infer I)=>void) ? I : never

type r = UnionToIntersection<nested[keyof nested]>
type Result = r[keyof r]

type Expected =
    | { name: "a"; eventName: "a1"; payload: string }
    | { name: "a"; eventName: "a2"; payload: number }
    | { name: "b"; eventName: "b1"; payload: boolean };

declare const expected: Expected;
declare const result: Result;
// In case types are not the same I expect a type error here
const y: Result = expected
const l: Expected = result

I wonder if there is an easier way.

I've started by creating a nested object type, but with the aim of putting name and eventName on the same level.

The intermediate result was something like this:

type nested = {
    a: {
        a1: {
            name: 'a',
            eventName: 'a1',
            payload: string
        },
        a2: {
            name: 'a',
            eventName: 'a2',
            payload: number
        }
    },
    b: {
        b1: {
            name: 'b',
            eventName: 'b1',
            payload: boolean
        }
    }
}

Then I've extracted a union of only the inner values types.

I've tried using things like Extract on eventName, but while it works for 'b1', it leads to never with 'a1' and 'a2'.

type b1 = Extract<Union, {event: {eventName: 'b1'}}> // ok
type a1 = Extract<Union, {event: {eventName: 'a1'}}> // never 
Sign up to request clarification or add additional context in comments.

3 Comments

In the end, I managed to achieve my goal by slighthly changing my initial data structure. But your solution is quite clever and works perfectly :)
@Ilario Pierbattista can I use your solution in my TS blog? I will put e reference to this question and your name
@IlarioPierbattista catchts.com/flatten-union
1

Much uglier solution than @Ilario Pierbattista, but works:

//https://stackoverflow.com/questions/50374908/transform-union-type-to-intersection-type/50375286#50375286
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
  k: infer I
) => void
  ? I
  : never;

//https://github.com/microsoft/TypeScript/issues/13298#issuecomment-468114901
type UnionToOvlds<U> = UnionToIntersection<
  U extends any ? (f: U) => void : never
>;

type PopUnion<U> = UnionToOvlds<U> extends (a: infer A) => void ? A : never;


type UnionToArray<T, A extends unknown[] = []> = IsUnion<T> extends true
  ? UnionToArray<Exclude<T, PopUnion<T>>, [PopUnion<T>, ...A]>
  : [T, ...A];

// https://stackoverflow.com/questions/53953814/typescript-check-if-a-type-is-a-union#comment-94748994
type IsUnion<T> = [T] extends [UnionToIntersection<T>] ? false : true;

type ArrayOfUnions<T> = IsUnion<T> extends true ? UnionToArray<T> : T

//https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types
type Distributive<T> = [T] extends [any] ? T : never

// treat this predicate as Array.prototype.map predicate
type MapPredicate<T> =
  T extends { event: object, name: string }
  ? IsUnion<T['event']> extends true
  ? ArrayOfUnions<T['event']> extends any[]
  ? Distributive<ArrayOfUnions<T['event']>[number]> & { name: T['name'] }
  : T
  : T['event'] & { name: T['name'] }
  : never
//https://catchts.com/tuples#map
// This util works similar to Array.prototype.map
type Mapped<
  Arr extends Array<unknown>,
  Result extends Array<unknown> = []
  > = Arr extends []
  ? []
  : Arr extends [infer H]
  ? [...Result, MapPredicate<H>]
  : Arr extends [infer Head, ...infer Tail]
  ? Mapped<[...Tail], [...Result, MapPredicate<Head>]>
  : Readonly<Result>;

type Union =
  | {
    name: "a";
    event:
    | { eventName: "a1"; payload: string }
    | { eventName: "a2"; payload: number };
  }
  | {
    name: "b";
    event: { eventName: "b1"; payload: boolean };
  };

type Result = Mapped<UnionToArray<Union>>[number]


type Expected =
  | { name: "a"; eventName: "a1"; payload: string }
  | { name: "a"; eventName: "a2"; payload: number }
  | { name: "b"; eventName: "b1"; payload: boolean };


type Assert = Result extends Expected ? true : false // true

Algorithm:

  1. Convert union type to array. See UnionToArray
  2. Iterate over the array and change all elements. See Mapped & Predicate
  3. Convert array to union

Playground

Here you can find more examples

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.