Typescript, merge object types?
Asked Answered
M

10

35

Is it possible to merge the props of two generic object types? I have a function similar to this:

function foo<A extends object, B extends object>(a: A, b: B) {
    return Object.assign({}, a, b);
}

I would like the type to be all the properties in A that does not exist in B, and all properties in B.

merge({a: 42}, {b: "foo", a: "bar"});

gives a rather odd type of {a: number} & {b: string, a: string}, a is a string though. The actual return gives the correct type, but I can not figure how I would explicitly write it.

Mississippian answered 5/4, 2018 at 22:37 Comment(2)
By 'explicitly' do you mean A & B? Is it what you're looking for?Woodcraft
Use intersection operator as follows: A & BBesides
A
63

UPDATE for TS4.1+

The original answer still works (and you should read it if you need an explanation), but now that recursive conditional types are supported, we can write merge() with to be variadic:

type OptionalPropertyNames<T> =
  { [K in keyof T]-?: ({} extends { [P in K]: T[K] } ? K : never) }[keyof T];

type SpreadProperties<L, R, K extends keyof L & keyof R> =
  { [P in K]: L[P] | Exclude<R[P], undefined> };

type Id<T> = T extends infer U ? { [K in keyof U]: U[K] } : never

type SpreadTwo<L, R> = Id<
  & Pick<L, Exclude<keyof L, keyof R>>
  & Pick<R, Exclude<keyof R, OptionalPropertyNames<R>>>
  & Pick<R, Exclude<OptionalPropertyNames<R>, keyof L>>
  & SpreadProperties<L, R, OptionalPropertyNames<R> & keyof L>
>;

type Spread<A extends readonly [...any]> = A extends [infer L, ...infer R] ?
  SpreadTwo<L, Spread<R>> : unknown

type Foo = Spread<[{ a: string }, { a?: number }]>

function merge<A extends object[]>(...a: [...A]) {
  return Object.assign({}, ...a) as Spread<A>;
}

And you can test it:

const merged = merge(
  { a: 42 },
  { b: "foo", a: "bar" },
  { c: true, b: 123 }
);
/* const merged: {
    a: string;
    b: number;
    c: boolean;
} */

Playground link to code

ORIGINAL ANSWER


The intersection type produced by the TypeScript standard library definition of Object.assign() is an approximation that doesn't properly represent what happens if a later argument has a property with the same name as an earlier argument. Until very recently, though, this was the best you could do in TypeScript's type system.

Starting with the introduction of conditional types in TypeScript 2.8, however, there are closer approximations available to you. One such improvement is to use the type function Spread<L,R> defined here, like this:

// Names of properties in T with types that include undefined
type OptionalPropertyNames<T> =
  { [K in keyof T]: undefined extends T[K] ? K : never }[keyof T];

// Common properties from L and R with undefined in R[K] replaced by type in L[K]
type SpreadProperties<L, R, K extends keyof L & keyof R> =
  { [P in K]: L[P] | Exclude<R[P], undefined> };

type Id<T> = T extends infer U ? { [K in keyof U]: U[K] } : never // see note at bottom*

// Type of { ...L, ...R }
type Spread<L, R> = Id<
  // Properties in L that don't exist in R
  & Pick<L, Exclude<keyof L, keyof R>>
  // Properties in R with types that exclude undefined
  & Pick<R, Exclude<keyof R, OptionalPropertyNames<R>>>
  // Properties in R, with types that include undefined, that don't exist in L
  & Pick<R, Exclude<OptionalPropertyNames<R>, keyof L>>
  // Properties in R, with types that include undefined, that exist in L
  & SpreadProperties<L, R, OptionalPropertyNames<R> & keyof L>
  >;

(I've changed the linked definitions slightly; using Exclude from the standard library instead of Diff, and wrapping the Spread type with the no-op Id type to make the inspected type more tractable than a bunch of intersections).

Let's try it out:

function merge<A extends object, B extends object>(a: A, b: B) {
  return Object.assign({}, a, b) as Spread<A, B>;
}

const merged = merge({ a: 42 }, { b: "foo", a: "bar" });
// {a: string; b: string;} as desired

You can see that a in the output is now correctly recognized as a string instead of string & number. Yay!


But note that this is still an approximation:

  • Object.assign() only copies enumerable, own properties, and the type system doesn't give you any way to represent the enumerability and ownership of a property to filter on. Meaning that merge({},new Date()) will look like type Date to TypeScript, even though at runtime none of the Date methods will be copied over and the output is essentially {}. This is a hard limit for now.

  • Additionally, the definition of Spread doesn't really distinguish between missing properties and a property that is present with an undefined value. So merge({ a: 42}, {a: undefined}) is erroneously typed as {a: number} when it should be {a: undefined}. This can probably be fixed by redefining Spread, but I'm not 100% sure. And it might not be necessary for most users. (Edit: this can be fixed by redefining type OptionalPropertyNames<T> = { [K in keyof T]-?: ({} extends { [P in K]: T[K] } ? K : never) }[keyof T])

  • The type system can't do anything with properties it doesn't know about. declare const whoKnows: {}; const notGreat = merge({a: 42}, whoKnows); will have an output type of {a: number} at compile time, but if whoKnows happens to be {a: "bar"} (which is assignable to {}), then notGreat.a is a string at runtime but a number at compile time. Oops.

So be warned; the typing of Object.assign() as an intersection or a Spread<> is kind of a "best-effort" thing, and can lead you astray in edge cases.

Playground link to code


*Note: Id<T> is an identity type and in principle shouldn't do anything to the type. Someone at some point edited this answer to remove it and replace with just T. Such a change isn't incorrect, exactly, but it defeats the purpose... which is to iterate through the keys to eliminate intersections. Compare:

type Id<T> = T extends infer U ? { [K in keyof U]: U[K] } : never 

type Foo = { a: string } & { b: number };
type IdFoo = Id<Foo>; // {a: string, b: number }

If you inspect IdFoo you will see that the intersection has been eliminated and the two constituents have been merged into a single type. Again, there's no real difference between Foo and IdFoo in terms of assignability; it's just that the latter is easier to read in some circumstances.

Avestan answered 6/4, 2018 at 0:40 Comment(6)
This looks good! The assign in my case will only be used on object literals, so most edge cases are fine! I do have an issue with your fix for OptionalPropertyNames. The Spread type complains, which. I would love to get fixed!Mississippian
Prettier removed the - which is key :)Mississippian
Is there a solution for merging 3+ objects into a single object type?Inveigle
You could, of course, use Spread<Spread<L,M>,R> or the like. I assume you want to take something like a tuple type [A,B,C,D,E,F] and automatically turn it into Spread<Spread<Spread<Spread<Spread<A,B>,C>,D>,E>,F>... but I don't think you can get that for arbitrary-length tuples due to recursion limits in the language. You can hardcode types for up to a certain length, like type SpreadTuple<T extends any[], L extends number = T['length']> = L extends 0 ? never : L extends 1 ? T[0] : L extends 2 ? Spread<T[0],T[1]> : ...Avestan
Has anyone released this as an NPM package?Unexacting
This has to be my favorite SO answer ever!Canzona
D
14

Update:

Thanks to the comment from re-gor I revisited this and updated the syntax to be more explicit about the merge.

type Merge<A, B> = {
  [K in keyof A | keyof B]: 
    K extends keyof A & keyof B
    ? A[K] | B[K]
    : K extends keyof B
    ? B[K]
    : K extends keyof A
    ? A[K]
    : never;
};

By extending the keyof operator to A and B individually, all the keys are exposed.

Using nested ternary types, first check to see if the key is present in A and B, and when it is, then the type is A[K] | B[K].

Next, when the key only comes from B, then the type is B[K].

Next, when the key only comes from A, then the type is A[K].

Finally the key does not exist in neither A nor B and the type is never.

type X = Merge<{ foo: string; bar: string }, { bar: number }>;

>>> type X = { foo: string; bar: string | number; }

Previous:

I found a syntax to declare a type that merges all properties of any two objects.

type Merge<A, B> = { [K in keyof (A | B)]: K extends keyof B ? B[K] : A[K] };

This type allows you to specify any two objects, A and B.

From these, a mapped type whose keys are derived from available keys from either object is created. The keys come from keyof (A | B).

Each key is then mapped to the type of that key by looking up the appropriate type from the source. If the key comes from B, then the type is the type of that key from B. This is done with K extends keyof B ?. This part asks the question, "is K a key from B" ? To get the type of that key, K, use a property lookup B[K].

If the key is not from B, it must be from A, thus the ternary is completed:

K extends keyof B ? B[K] : A[K]

All of this is wrapped in an object notation { }, making this a mapped object type, whose keys are derived from two objects and whose types map to the source types.

Discontented answered 20/1, 2021 at 17:1 Comment(2)
Love this answer. Simple to use and straight to the point.Predestinate
Sadly it's not working properly in some cases. For example, if one wants merge {foo: string, bar: string} and {bar: number} it resolves to {bar: number} instead of {foo: string, bar: number}. One field has been lostShirley
C
4

tl;dr

type Expand<T> = T extends object
  ? T extends infer O
    ? { [K in keyof O]: O[K] }
    : never
  : T;

type UnionToIntersection<U> = Expand<
  (U extends any ? (k: U) => void : never) extends (k: infer I) => void
    ? I
    : never
>;

const merge = <A extends object[]>(...a: [...A]) => {
  return Object.assign({}, ...a) as UnionToIntersection<A[number]>;
};

Motivation for yet another answer

jcalz answer is good and worked for me for years. Unfortunately, as the count of merged objects grows to a certain number, typescript produces the following error:

Type instantiation is excessively deep and possibly infinite. [2589]

and fails to deduce the resulting object type. This happens due to the typescript issue that has been discussed excessively at the following github issue: https://github.com/microsoft/TypeScript/issues/34933

Detail

In merge() code above A[number] type expands to union of array element types. UnionToIntersection metafunction converts the union to intersection. Expand flattens the intersection so it becomes more readable by IntelliSense-like tools.

See the following references for more details on UnionToIntersection and Expand implementations:
https://mcmap.net/q/76498/-transform-union-type-to-intersection-type
https://github.com/shian15810/type-expand

Extra

When using merge() function, it is likely that key duplicates in merged objects is an error. The following function can be used to find such duplicates and throw Error:

export const mergeAssertUniqueKeys = <A extends object[]>(...a: [...A]) => {
  const entries = a.reduce((prev, obj) => {
    return prev.concat(Object.entries(obj));
  }, [] as [string, unknown][]);

  const duplicates = new Set<string>();
  entries.forEach((pair, index) => {
    if (entries.findIndex((p) => p[0] === pair[0]) !== index) {
      duplicates.add(pair[0]);
    }
  });

  if (duplicates.size > 0) {
    throw Error(
      [
        'objects being merged contain following key duplicates:',
        `${[...duplicates].join(', ')}`,
      ].join(' '),
    );
  }

  return Object.assign({}, ...a) as UnionToIntersection<A[number]>;
};
Craigie answered 2/11, 2022 at 16:6 Comment(1)
This worked great for my use case where key duplicates between the merged objects were not allowed. Just want to emphasize that the caveat for this solution is that it will not work when keys are duplicated between the objects with types that do not overlap (e.g., merging { a: string } and { a: number })Indult
I
2

If you want to preserve property order, use the following solution.

See it in action here.

export type Spread<L extends object, R extends object> = Id<
  // Merge the properties of L and R into a partial (preserving order).
  Partial<{ [P in keyof (L & R)]: SpreadProp<L, R, P> }> &
    // Restore any required L-exclusive properties.
    Pick<L, Exclude<keyof L, keyof R>> &
    // Restore any required R properties.
    Pick<R, RequiredProps<R>>
>

/** Merge a property from `R` to `L` like the spread operator. */
type SpreadProp<
  L extends object,
  R extends object,
  P extends keyof (L & R)
> = P extends keyof R
  ? (undefined extends R[P] ? L[Extract<P, keyof L>] | R[P] : R[P])
  : L[Extract<P, keyof L>]

/** Property names that are always defined */
type RequiredProps<T extends object> = {
  [P in keyof T]-?: undefined extends T[P] ? never : P
}[keyof T]

/** Eliminate intersections */
type Id<T> = { [P in keyof T]: T[P] }
Inveigle answered 7/3, 2019 at 20:48 Comment(0)
A
2

Probably all the answers are correct, but I built a shorter Merge generic which does what u need:

type Merge<T, K> = Omit<T, keyof K> & K;
Archicarp answered 12/5, 2023 at 5:32 Comment(0)
Q
0

I think you're looking for more of a union (|) type instead of an intersection (&) type. It's closer to what you want...

function merge<A, B>(a: A, b: B): A | B {
  return Object.assign({}, a, b)
}

merge({ a: "string" }, { a: 1 }).a // string | number
merge({ a: "string" }, { a: "1" }).a // string

learning TS I spent a lot of time coming back to this page... it's a good read (if you're into that sort of thing) and gives a lot of useful information

Quincy answered 5/4, 2018 at 23:11 Comment(2)
This is unlikely true. You can try merge({a: 42}, {b: "foo", a: "bar"}).b from the OP.Woodcraft
In your first example the result of the merge will be {a: 1}. Using an intersection correctly types this as {a: number}Sastruga
U
0

I like this answer from @Michael P. Scott.

I did it a little simpler since I was also looking for it. Let me share and explain it step by step.

  1. Use the A type as the base for the merge type.
  2. Get the keys of B that are not in A (a utility type like Exclude will help).
  3. Finally, intersects types from steps #1 and #2 with & to get the combined type.
type Merge<A, B> = A & { [K in Exclude<keyof B, keyof A>]: B[K] };
Utham answered 27/12, 2022 at 0:27 Comment(0)
S
0

Previous Answer is nice, but it is not actually accurate

This one is a bit better:

type Merge<A extends {}, B extends {}> = { 
  [K in keyof (A & B)]: (
    K extends keyof B 
      ? B[K] 
      : (K extends keyof A ? A[K] : never) 
  )
};

Difference can be checked in the playground

Shirley answered 16/1, 2023 at 16:55 Comment(0)
C
0

This solution work as well

type Merge<T extends object[]> = T extends [infer First, ...infer Rest]
    ? First extends object
        ? Rest extends object[]
            ? Omit<First, keyof Merge<Rest>> & Merge<Rest>
            : never
        : never
    : object

export const merge = <T extends object[]>(...objects: T): Merge<T> => Object.assign({}, ...objects)
Cuevas answered 31/3, 2024 at 9:6 Comment(0)
I
0

Found this little hack which cleans up the merged types e.g.

type MergeKeys<Obj> = [{
    [Key in keyof Obj]: Obj[Key]
}][0]

Example usage

type User = MergeKey<{ id: number } & { name: string } & { active: boolean }>

// outputs { id: number, name: string, active: boolean } 

NOTE: Duplicate keys may cause problem.

Try it on the TypeScript playground!

Isolate answered 20/6, 2024 at 22:45 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.