Search code examples
typescripttypes

Merge Two Union Types and Create Object Typescript


I just moved from javascript to typescript and I am on the learning phase. I have two union types and from them I want to make one type.

I tried everything, Exclude and Extract Type also but no luck.

Below is my code with expected result and actual result.

type Union1 = 'key1' | 'key2';

type Union2 = 'value1' | 'value2';

type MergeObj<K extends string | number, V> = {
  [T in V as K]: T;
};

type Result = MergeObj<Union1, Union2>;

// expected: { key1: 'value1', key2: 'value2' }
// actual: { key1: 'value1' | 'value2', key2: 'value1' | 'value2' }

Typescript: https://www.typescriptlang.org/play?#code/C4TwDgpgBAqgdgSwPZwIxQLxQOQGsIirZQA+O+IATNgNwBQdoksiKlmOAbgIYA2ArhCKkufQdXqNw0ALIQATgHMIAeQBGAKwA8AaSgQAHsAhwAJgGco54PIRxFIuPwC2ahQBooANQB8HAN50UFAA2gAqUHbeUNyWOgC6AFxQYfQAvpJM0ABKEOb8vMAcckqqmlrwyGielWw+kgD0DfoGkADGxqbJ-lAUqMnYPAJC2J4UlAND4sRpdE0xHfx83b0E-aLDwmSDYhDUYwQTG4Jbx3szQA

Edit 1

In real case, I have two union with the mixed type as below

type Union1 = 'key1' | 'key2' | 'key3' | 4 | 'key5';

type Union2 = 'value1' | 'value2' | 3 | 'value4' | 'value5';

so the union can have string or number and the union will have same amount of value always. ie. currently in example, I have 5.

so I want to convert from those 2 union type to one type as below.

type Result = {
  key1: 'value1';
  key2: 'value2';
  key3: 3;
  4: 'value4';
  key5: 'value5';
}

Solution

  • This is essentially impossible, for the same reason it's impossible to turn union types into tuple types (see How to transform union type to tuple type): union types in TypeScript are unordered (or, more accurately, their order is an implementation detail and cannot be relied upon). The type type U = 1 | 2 is completely equivalent to type U = 2 | 1 and the compiler is free to change one to the other. So correlating two unions by "index" or "position" or "order" is impossible, since you can't tell the difference between type Keys = "key1" | "key2" and type Keys = "key2" | "key1".

    If you had tuples of keys and values, you could iterate through them to get the object you want. Indeed we could go through the motions of teasing apart the compiler-determined order of a union in order to implement your MergeObj type, something like this:

    // get the "Last" element in a union
    type Last<T> =
      ((T extends any ? ((x: (() => T)) => void) : never) extends
        ((x: infer I) => void) ? I : never) extends () => infer U ? U : never
    
    // convert a union to a tuple
    type UnionToTuple<T, A extends any[] = []> =
      [T] extends [never] ? A : UnionToTuple<Exclude<T, Last<T>>, [Last<T>, ...A]>
    
    // merge key/value unions by first turning them to tuples
    type MergeObj<K extends PropertyKey, V> =
      [UnionToTuple<K>, UnionToTuple<V>] extends [infer KK extends PropertyKey[], infer VV extends any[]] ?
      { [P in keyof KK & keyof VV & `${number}` as KK[P]]: VV[P] } : never
    

    I'm not going to detail how that works, because it's beside the point. Let's try it out:

    // type Nums = 1 | 2 | 3 | 4 | 5; // 😇    
    
    type Union1 = 'key1' | 'key2' | 'key3' | 4 | 'key5';
    type Union2 = 'value1' | 'value2' | 3 | 'value4' | 'value5';
    
    type O = MergeObj<Union1, Union2>
    /* type O = {
        key1: "value1";
        key2: "value2";
        key3: 3;
        4: "value4";
        key5: "value5";
    } */
    

    Hey, that works exactly as you want, right? So what's the problem? Well, if you look at the commented-out line // type Nums = 1 | 2 | 3 | 4 | 5; // 😇 If we uncomment that line back in, something happens:

    type Nums = 1 | 2 | 3 | 4 | 5; // 😈    
    
    type Union1 = 'key1' | 'key2' | 'key3' | 4 | 'key5';
    type Union2 = 'value1' | 'value2' | 3 | 'value4' | 'value5';
    
    type O = MergeObj<Union1, Union2>
    /* type O = {
        4: 3;
        key1: "value1";
        key2: "value2";
        key3: "value4";
        key5: "value5";
    } */
    

    The resulting object type has changed, due only to a change in seemingly unrelated code. The compiler feels free to represent unions however it likes and in whatever order it likes. It turns out that by making explicit reference to 1 | 2 | 3 | 4 | 5 earlier in the code than before Union1 and Union2 were defined, the compiler will tend to list these first in any future unions it encounters:

    // type Union1 = 4 | "key1" | "key2" | "key3" | "key5"
    // type Union2 = 3 | "value1" | "value2" | "value4" | "value5"
    

    And that ruins everything. There's not much point in continuing here.


    To recap: there is simply no way to define Union1 and Union2 as unions and then reliably correlate them based on "index", "position", or "order". You should find a completely different approach, such as recording the desired order in an ordered data structure like a tuple; or start with the desired object and derive the unions from them; or give up entirely, depending on your use case.

    Playground link to code