typed-diff
v1.0.0
Published
A library that creates a diff object by comparing two objects recursively, with type-level path narrowing.
Maintainers
Readme
typed-diff
A library that recursively diffs two objects, with type-level path narrowing.
Installation
npm install typed-diffRequirements
- TypeScript >= 5.0 (only when using the package from TypeScript; there is no runtime dependency)
Usage
import { diff } from "typed-diff";
const x = {
nested: {
a: "1",
b: 2,
c: 3,
str: "before",
bool: true,
list: [{ 1: "test" }, { 2: "before" }, 3],
},
};
const y = {
nested: {
a: "1",
b: 3,
d: 4,
str: "after",
bool: false,
list: [{ 1: "test" }, { 2: "after" }, 3, 4],
},
};
const result = diff(x, y);The result constant becomes an array of objects that represent the changes between x and y, like so:
[
{
operation: "update",
path: "nested.b",
before: 2,
after: 3,
},
{
operation: "delete",
path: "nested.c",
before: 3,
after: undefined,
},
{
operation: "update",
path: "nested.str",
before: "before",
after: "after",
},
{
operation: "update",
path: "nested.bool",
before: true,
after: false,
},
{
operation: "update",
path: "nested.list.1.2",
before: "before",
after: "after",
},
{
operation: "add",
path: "nested.list.3",
before: undefined,
after: 4,
},
{
operation: "add",
path: "nested.d",
before: undefined,
after: 4,
},
];And, TypeScript is supported, so you can use the diff constant like so:
you can add a type assertion to the diff function
import { diff } from "typed-diff";
type X = {
nested: {
foo: string;
bar: string;
list: Array<{ foo: string }>;
};
};
type Y = {
nested: {
foo: string;
baz: string;
list: Array<{ foo: string }>;
};
};
const x: X = {
nested: {
foo: "1",
bar: "before",
list: [{ foo: "test" }],
},
};
const y: Y = {
nested: {
foo: "1",
baz: "after",
list: [{ foo: "test" }],
},
};
const result = diff(x, y);
result.forEach((r) => {
if (r.path === "nested.foo") {
console.log(r.before); // r.before type becomes string | undefined
console.log(r.after); // r.after type becomes string | undefined
} else if (r.path === "nested.baz" && r.operation === "add") {
console.log(r.before); // r.before type becomes undefined
console.log(r.after); // r.after type becomes string
} else if (r.path === "nested.list.0.foo" && r.operation === "delete") {
console.log(r.before); // r.before type becomes string
console.log(r.after); // r.after type becomes undefined
}
});Both type parameters of diff are const type parameters, so inline literal
arguments keep their literal types:
const result = diff({ mode: "dark" }, { mode: "light" });
result.forEach((r) => {
if (r.path === "mode" && r.operation === "update") {
console.log(r.before); // typed "dark", not string
console.log(r.after); // typed "light", not string
}
});Limitations
- Arrays are diffed by index. Inserting an element at the head of an array is reported as an update of every following index plus one
add, not as a single insertion. - Keys containing
.are not supported. Paths are dot-joined strings, so a key like"a.b"is indistinguishable from the nested patha.b. undefinedvalues are treated the same as missing keys.diff({ a: undefined }, {})returns[].- Functions and symbols are never compared. Two functions (or two symbols) at the same path are always treated as equal; replacing a function with a value of another type is reported as an update.
- Circular references are not supported.
diffthrows aTypeErrorwhen either input contains a cycle. Shared (non-circular) references are fine.
