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.

Answer from jcalz on Stack Overflow
Top answer
1 of 11
73

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.

2 of 11
15

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.

๐ŸŒ
Aaron Bos
aaronbos.dev โ€บ posts โ€บ merge-objects-typescript-javascript
Merging Objects in TypeScript (and JavaScript)
June 11, 2022 - The reason for this being is that shallow merging pertains to simply copying property values from one or many objects to another. If a property is a reference type like an object or array, then the reference will be copied instead of creating entirely new objects. The two most common methods of shallow merging in TS and JS are Object.assign() and using the ...
Discussions

How to combine object properties in typescript? - Stack Overflow
Use Typescript spread operator it transpile to Javascript Object.assign() If you need deep tree object merging you could use changing function of best-global package ... Save this answer. ... Show activity on this post. Lodash has an "extend" function that combines objects and lets Typescirpt know that the new object has the type you'd expect. const a = { one: 1, two... More on stackoverflow.com
๐ŸŒ stackoverflow.com
typescript - How to correctly merge two object types? - Stack Overflow
I am working on a TypeScript generic for merging objects, but I encountered an issue where test case t11 is failing. I would like to understand the reason behind it and how to modify More on stackoverflow.com
๐ŸŒ stackoverflow.com
How can I keep an optional property in a merged type where there is an overlap?
You have to Omit the property you want to replace for this to work More on reddit.com
๐ŸŒ r/typescript
9
10
August 16, 2022
merge 2 Maps?

You can use multiple spread operators in one object: new Map({...firstMap, ...secondMap});

Not sure what will happen if they have keys in common. secondMap's values will probably overwite firstMap's

More on reddit.com
๐ŸŒ r/typescript
7
7
November 16, 2020
๐ŸŒ
Omarileon
omarileon.me โ€บ blog โ€บ typescript-merge-objects
mari. | 3 Simple Ways to Merge Objects in TypeScript
January 26, 2024 - Instead of a merging all of the ... So to sum things up, to merge objects in TypeScript, you can use either the spread syntax, Lodash's merge function, or Object.assign....
๐ŸŒ
Ultimate Courses
ultimatecourses.com โ€บ blog โ€บ use-intersection-types-to-combine-types-in-typescript
Use Intersection Types to Combine Types in TypeScript - Ultimate Courses
July 7, 2022 - interface Order { total: number; currency: string; } interface Stripe { card: number; } interface PayPal { email: string; } type StripeOrder = Order & Stripe; type PayPalOrder = Order & PayPal; const isStripe = (order: StripeOrder | PayPalOrder): order is StripeOrder => { return 'card' in (order as StripeOrder); }; const processOrder = (order: StripeOrder | PayPalOrder) => { if (isStripe(order)) { // โœ… order.card // โŒ order.email } else { // โœ… order.email // โŒ order.card } }; And thatโ€™s it, a nice little example of merging types in TypeScript to create more types!
๐ŸŒ
SPGuides
spguides.com โ€บ merge-two-objects-in-typescript
How to Merge Two Objects in TypeScript?
February 7, 2024 - Type Inference: TypeScriptโ€™s type system automatically infers the resulting objectโ€™s type and will raise errors if there are attempts to add or remove properties that do not conform to the inferred type. In this Typescript tutorial, I have explained how to merge two objects in Typescript using different methods.
๐ŸŒ
Medium
medium.com โ€บ @glasshost โ€บ how-to-merge-objects-in-typescript-52e2d64e77b6
How to merge Objects in TypeScript | by Glasshost | Medium
April 14, 2023 - const obj1 = { a: 1, b: 2 }; const obj2 = { b: 3, c: 4 }; const mergedObj = Object.assign(obj1, obj2); console.log(mergedObj); // Output: { a: 1, b: 3, c: 4 } In the example above, we merged `obj2` into `obj1` using `Object.assign()`. Note that the `Object.assign()` method mutates the target object (`obj1`), so we need to make a copy of the source object if we want to keep the original object.
Find elsewhere
๐ŸŒ
TypeScript
typescriptlang.org โ€บ docs โ€บ handbook โ€บ declaration-merging.html
TypeScript: Documentation - Declaration Merging
TypeScript uses declaration merging to build up definitions like this in a type-safe way. ... Not all merges are allowed in TypeScript. Currently, classes can not merge with other classes or with variables. For information on mimicking class merging, see the Mixins in TypeScript section. Although JavaScript modules do not support merging, you can patch existing objects by importing and then updating them.
๐ŸŒ
GeeksforGeeks
geeksforgeeks.org โ€บ typescript โ€บ how-to-deep-merge-two-objects-in-typescript
How to Deep Merge Two Objects in TypeScript ? - GeeksforGeeks
June 16, 2025 - In this article, we will explore various approaches to deep merging objects in TypeScript, along with their syntax and examples. ... This approach involves recursively traversing the objects and merging their properties. When encountering nested objects, the function calls itself to perform a deep merge. function deepMerge<T>(target: T, ...sources: Partial<T>[]): T { // Implementation } In this example, we have two objects obj1 and obj2 with nested properties.
๐ŸŒ
Bobby Hadz
bobbyhadz.com โ€บ blog โ€บ typescript-merge-objects
How to merge Objects in TypeScript | bobbyhadz
Use the spread syntax (...) to merge objects in TypeScript. The type of the final object will successfully be inferred, so trying to add or remove properties causes the type checker to show an error.
๐ŸŒ
Tim Mousk
timmousk.com โ€บ blog โ€บ typescript-merge-objects
How To Merge Objects In TypeScript? โ€“ Tim Mouskhelichvili
March 27, 2023 - Here is an example of merging two objects using Lodash: typescriptimport { merge } from 'lodash'; const obj1 = { startYear: 2000 }; const obj2 = { endYear: 2022 }; const merged = merge(obj1, obj2); // Outputs: '{ endYear: 2022, startYear: 2000 }' console.log(merged);
๐ŸŒ
Squash
squash.io โ€บ tutorial-on-merging-objects-in-typescript
How to Merge Objects in TypeScript - Squash Labs
May 8, 2024 - TypeScript provides several techniques ... merging two objects, merging properties of objects, object spreading, using Object.assign, merging objects with same keys, merging objects with different keys, and merging nested objects...
๐ŸŒ
DEV Community
dev.to โ€บ fullstackchris โ€บ advanced-typescript-a-generic-function-to-merge-object-arrays-18ja
Advanced TypeScript: A Generic Function to Merge Object Arrays - DEV Community
December 9, 2021 - You could explicitly write key / value assignments for the keys that you want to update... or you can leverage JavaScript's built in Object.assign function and TypeScript's generic capabilities to only write one such function for all merging actions you need across your entire app! ๐Ÿ˜„ ยท For example, in ReduxPlate, I have two types, IFile, and IEditorSettings: ... IEditorSettings extends IFile and has just one additional property: isActive. When visitors click the "Generate!" button on the MVP page, the response from the server returns an array of objects of type IFile instead of IEditorSettings, since the server is not concerned with the isActive property.
๐ŸŒ
Reactgo
reactgo.com โ€บ home โ€บ how to merge the two objects in typescript
How to merge the two objects in TypeScript | Reactgo
September 5, 2023 - To merge the two objects into a single object, we can use the es6 spread(โ€ฆ) operator in TypeScript.
๐ŸŒ
DEV Community
dev.to โ€บ svehla โ€บ typescript-how-to-deep-merge-170c
How to deep merge in Typescript - DEV Community
October 5, 2021 - This generic finds all nonshared keys between object T and U and makes them optional thanks to Partial<> generic provided by Typescript. This type with Optional keys is merged via & an operator with the object that contains all T and U shared keys which values are of type T[K] | U[K].
Top answer
1 of 1
1

The any type is intentionally unsound. It acts as both a top type like unknown and as (nearly) a bottom type like the never type. any is assignable both to and from every other type (except that any is not assignable to never). That means many of the conditional types in your code will behave differently from your expectations if the type being checked is any. Your U[K] extends object check passes if U[K] is any. So does the U[K] extends any[] check. And the U[K] extends any check always passes.

So when U is {a: any} then your example code decides that the a property is an array and returns it. Oops.

If you want to handle any in a special way, you'll need to change your check.

The only reliable way I know how to do this is as described in Typescript check for the 'any' type and Disallow call with any and it takes advantage of the unsoundness. TypeScript will convert just about every union and intersection with any to any. Something like 1 extends (0 & T) seems like it's always false, since supposedly X extends (Y & Z) requires both X extends Y and Y extends Z. And since 1 extends 0 is false, that should always be false too. But if T is any, then 0 & T is also any (unsound, unsafe, but intended), and then 1 extends any is true. So you can use the unsoundness to detect any.

I will rewrite some relevant bits of your code:

type MergeTypesWithPriority<T, U> = 1 extends (T & U & 0) ? T : { // <--
    [K in keyof T | keyof U]: K extends keyof T
        ? K extends keyof U
            ? T[K] extends object
                ? U[K] extends object
                    ? T[K] extends any[]
                        ? T[K]
                        : U[K] extends any[]
                            ? 1 extends (U[K] & 0) ? T[K] : U[K] // <--
                            : MergeTypesWithPriority<T[K], U[K]>
                    : T[K]
                : T[K]
            : T[K]
        : K extends keyof U
            ? U[K] extends object
                ? T extends Record<K, any> 
                    ? 1 extends (U[K] & 0) // <--
                        ? T[K]
                        : U[K]
                    : U[K]
                : U[K]
            : never;
};

The check at the top bails out early if either T or U are any, and returns T if so. The check in the middle fixes the problem you had with misclassifying any as an array, by just returning T[K] if U[K] is any before proceeding. The check at the bottom changes your U[K] check so it actually looks for any (although I didn't see any examples where it matters).

Let's test your code:

type t11 = Expect<Equal<
  MergeTypesWithPriority<{ a: Items }, { a: any }>, 
  { a: Items }>
>; // okay

All your other tests pass, and now this one does also.

Playground link to code

๐ŸŒ
DEV Community
dev.to โ€บ techsquidtv โ€บ combine-objects-in-typescript-3f64
Combine Objects In TypeScript - DEV Community
April 11, 2021 - If you Google how to combine multiple objects into a single object with shared properties, you will get some information back on using the spread operator. Let's look at a basic example of the spread operator with TypeScript. In this example, we will be creating Super Heros by combining our regular Heros with SuperPowers to result in a single Super Hero. Begin by creating an Interface for each of the two objects.
๐ŸŒ
Delft Stack
delftstack.com โ€บ home โ€บ howto โ€บ typescript โ€บ typescript merge objects
How to Merge Objects TypeScript | Delft Stack
February 15, 2024 - Now, letโ€™s reverse the objects and check the result as shown below. # typescript var languages = {...languageTwo, ...languageOne}; console.log(languages.PLC); console.log(languages.PLF); ... As we can see from the above example, while merging the two objects, the properties present in both objects are updated based on the order of the objects in which we are merging them.
๐ŸŒ
xjavascript
xjavascript.com โ€บ blog โ€บ typescript-merge-two-objects
Merging Two Objects in TypeScript: A Comprehensive Guide โ€” xjavascript.com
The spread operator (...) is a concise and popular way to merge objects in TypeScript. It allows you to expand the properties of an object into another object. // Define two objects const obj1 = { a: 1, b: 2 }; const obj2 = { b: 3, c: 4 }; // ...
๐ŸŒ
Sling Academy
slingacademy.com โ€บ home โ€บ typescript โ€บ how to merge multiple objects in typescript (2 approaches)
How to Merge Multiple Objects in TypeScript (2 Approaches) - Sling Academy
December 3, 2023 - TypeScript is smart and is capable of inferring the types of the results, as you can see in the screenshot below: There is a small difference here. The types of the combined objects are Intersection types that are defined using an ampersand &.