type AnyObject = Record<PropertyKey, unknown>;
// properties of X also present in Y
type Intersection<X extends AnyObject, Y extends AnyObject> = {
[K in keyof X as K extends keyof Y ? K : never]: X[K];
};
// returns never if there is an intersection between X and Y,
// returns the conjunction of X and Y otherwise.
type NoIntersection<X extends AnyObject, Y extends AnyObject> = keyof Intersection<
X,
Y
> extends never
? X & Y
: never;
type StrictMerge<T extends Record<PropertyKey, unknown>[]> = T extends [infer X extends AnyObject]
? X
: T extends [infer X extends AnyObject, infer Y extends AnyObject]
? NoIntersection<X, Y>
: T extends [
infer X extends AnyObject,
infer Y extends AnyObject,
...infer Z extends AnyObject[],
]
? StrictMerge<[NoIntersection<X, Y>, ...Z]>
: never;
// function to mimic spread operator.Z
function strictMerge<Z extends AnyObject[]>(...sources: Z): StrictMerge<Z> {
// you could implement runtime validation. but this is fine
// for now since you said just typescript or eslint.
return Object.assign({}, ...sources) as StrictMerge<Z>;
}
// sample use.
const a = strictMerge(
{
x: 'foo',
},
{
y: 'bar',
},
{
z: 'baz',
},
{
i: 'foo',
},
{
j: 'bar',
},
);
// never
const b = strictMerge(
{
x: 'foo',
},
{
y: 'bar',
},
{
y: 'baz',
},
);
this does it by making sure all the objects does not contain keys of the other objects provided.