Merging Types in TypeScript
Consider the following types in TypeScript:
1type ObjA = {
2 propA: string;
3 propB: string;
4}
5
6type ObjB = {
7 propA: number;
8 propC: number;
9}
What if you want to create a type combining those two? A first approach could simply be using the union operator:
1type CombinedAB = ObjA | ObjB
This results in a Record type having all three properties propA
, propB
and propC
. However, you can ask yourself about the type of these properties? Surely propB
is of type string
and propC
is of type number
, but what about propA
? Ideally it should result in string | number
but in fact it's only string
. So using the union operator does not merge the inner types of a Record it just takes the first type it finds for properties with the same name. type CombinedBA = ObjB | ObjA
would result in propA
being of type number
.
1const combined: CombinedAB = {
2 propA: 123, // => TypeError: propA is of type string
3 propB: 'hello world!',
4 propC: 3.14159,
5}
But what if we want to combine (or union) the types of a Record's property? Here is a simple helper type for achieving that:
1type Merge<A, B> = {
2 [K in keyof A | keyof B]: K extends keyof A
3 ? K extends keyof B
4 ? A[K] | B[K]
5 : A[K]
6 : K extends keyof B
7 ? B[K]
8 : never;
9};
10
11type Merged = Merge<ObjA, ObjB>;
12// result => { propA: string | number, propB: string, propC: number }
At first this looks very cryptic but it's actually quite simple. At the root level it is an index type (they look like this { [s: string]: any }
). Now the keys of the new type have to be a union of the keys of the two base types so that the resulting type has every property from it's base types. Determining the types of the values now is just checking if the key K
is found in one or both of the base types using some ternary expressions. With this type we can create objects where the property types are unioned:
1const combinedA: CombinedAB = {
2 propA: 123, // totally fine now
3 propB: 'hello world!',
4 propC: 3.14159,
5}
6
7const combinedB: CombinedAB = {
8 propA: 'I can be a string', // and this still works
9 propB: 'hello world!',
10 propC: 3.14159,
11}
By understanding TypeScript’s type system, one can make good use of it and avoid many common errors that could make it to production otherwise.