简体   繁体   中英

How to flatten nested union types in Typescript?

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?

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 

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

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?

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM