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' }
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';
}
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.