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 thatmerge({},new Date())will look like typeDateto TypeScript, even though at runtime none of theDatemethods will be copied over and the output is essentially{}. This is a hard limit for now.Additionally, the definition of
Spreaddoesn't really distinguish between missing properties and a property that is present with an undefined value. Somerge({ a: 42}, {a: undefined})is erroneously typed as{a: number}when it should be{a: undefined}. This can probably be fixed by redefiningSpread, but I'm not 100% sure. And it might not be necessary for most users. (Edit: this can be fixed by redefiningtype 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 ifwhoKnowshappens to be{a: "bar"}(which is assignable to{}), thennotGreat.ais 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.
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 thatmerge({},new Date())will look like typeDateto TypeScript, even though at runtime none of theDatemethods will be copied over and the output is essentially{}. This is a hard limit for now.Additionally, the definition of
Spreaddoesn't really distinguish between missing properties and a property that is present with an undefined value. Somerge({ a: 42}, {a: undefined})is erroneously typed as{a: number}when it should be{a: undefined}. This can probably be fixed by redefiningSpread, but I'm not 100% sure. And it might not be necessary for most users. (Edit: this can be fixed by redefiningtype 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 ifwhoKnowshappens to be{a: "bar"}(which is assignable to{}), thennotGreat.ais 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.
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.
How to combine object properties in typescript? - Stack Overflow
typescript - How to correctly merge two object types? - Stack Overflow
How can I keep an optional property in a merged type where there is an overlap?
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
Seems like this should do the trick:
var objectA = {
propertyA: 1,
propertyB: 2,
.
. // more properties here
.
propertyM: 13
};
var objectB = {
propertyN: 14,
propertyO: 15,
.
. // more properties here
.
propertyZ: 26
};
var objectC = {...objectA, ...objectB}; // this is the answer
var a = objectC.propertyA;
var n = objectC.propertyN;
Based on this article: https://blog.mariusschulz.com/2016/12/23/typescript-2-1-object-rest-and-spread
In addition, the order of the variables in the decomposition matters. Consider the following:
var objectA = {
propertyA: 1,
propertyB: 2, // same property exists in objectB
propertyC: 3
};
var objectB = {
propertyX: 'a',
propertyB: 'b', // same property exists in objectA
propertyZ: 'c'
};
// objectB will override existing properties, with the same name,
// from the decomposition of objectA
var objectC = {...objectA, ...objectB};
// result: 'b'
console.log(objectC.propertyB);
// objectA will override existing properties, with the same name,
// from the decomposition of objectB
var objectD = {...objectB, ...objectA};
// result: '2'
console.log(objectD.propertyB);
(Is there an operator that can extract the interface/type of an existing object? Is it possible?)
You should go for typeof.
type typeA = typeof objectA;
type typeB = typeof objectB;
To get them merged you can use intersection operation as basarat already pointed out.
type typeC = typeA & typeB