Skip to content

Instantly share code, notes, and snippets.

@lumie1337
Last active December 13, 2018 22:34
Show Gist options
  • Save lumie1337/ec43560159b34b373c23b7306f5f85ce to your computer and use it in GitHub Desktop.
Save lumie1337/ec43560159b34b373c23b7306f5f85ce to your computer and use it in GitHub Desktop.

Deep Merge Semantics are:

  • keys of objects get merged { a: .. , b: .. } with { b: .. , c: .. }
    • the value of b needs to be merged; recurse
  • elements of arrays get
  • primitives in left and right that are distinct, produce a type error

so

  • objects have set semantics; recursive set merge
  • arrays have sequence semantics; order is important
  • certain objects produce nonsensical merge results
// TODO
type DeepMerge<A extends unknown, B extends unknown> = any //A & B
type Any = AnyVal | AnyRef
type Skip = undefined | null
type AnyRef = object
type AnyVal = bigint | number | string | symbol | boolean
type ObjectLiteral = Record<string, unknown>
function isNull(a: any): a is Skip {
if (typeof (a) === "undefined" || a === null)
return true;
return false;
}
function isAnyVal(a: unknown): a is AnyVal {
switch (typeof (a)) {
case "bigint":
case "boolean":
case "number":
case "string":
case "symbol":
return true;
}
return false;
}
class AssertError extends Error { }
function assert(cond: boolean, err: string = "") {
if (!cond)
throw new AssertError(err)
}
function isCompatible(a: any, b: any) {
if (isNull(a) || isNull(b)) // nullable values are always assignable
return true;
if (typeof (a) !== typeof (b))
return false;
if (a.prototype !== b.prototype)
return false;
// TODO implement more specific checks
return true;
}
function isObjectLiteral(a: unknown): a is ObjectLiteral {
return Object.getPrototypeOf(a) === Object.getPrototypeOf({});
}
function isArray<T>(a: unknown): a is Array<T> {
return (a instanceof Array);
}
function union<T>(a: T[], b: T[]): T[] {
return Array.from(new Set(a.concat(b)));
}
function difference<T>(a: T[], b: T[]): T[] {
return Array.from(a).filter(k => b.includes(k))
}
function deepMerge<A extends Any, B extends Any>(a: A, b: B): DeepMerge<A, B> {
assert(isCompatible(a, b), "deepMerge: a and b have incompatible types")
if (isNull(a))
return b;
if (isNull(b))
return a;
if (isAnyVal(a) && isAnyVal(b)) {
// XXX: typescript does not properly narrow types here
assert((a as any) === b, "deepMerge: a and b are AnyVals and must be equal")
return a;
}
assert(typeof (a) !== "function", "deepMerge: cannot merge functions")
if (isArray(a)) {
return a.concat(b);
}
if (isObjectLiteral(a)) {
assert(isObjectLiteral(b), "deepMerge: can only merge object literals with other object literals (for now)")
const result: any = {}
const commonKeys = union(Object.keys(a), Object.keys(b))
for (const key in commonKeys) {
// XXX: make typescript happy
result[key] = deepMerge(a[key] as any, b[key] as any)
}
for (const key in difference(Object.keys(a), commonKeys)) {
result[key] = a[key];
}
for (const key in difference(Object.keys(b), commonKeys)) {
result[key] = b[key];
}
// TODO: handle Symbol
return result;
}
assert(false, "deepMerge: not implemented data types")
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment