Skip to content

Latest commit

 

History

10 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

typed-diff

A library that recursively diffs two objects, with type-level path narrowing.

Installation

npm install typed-diff

Requirements

  • 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 path a.b.
  • undefined values 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. diff throws a TypeError when either input contains a cycle. Shared (non-circular) references are fine.

About

ts generate diff recursively lib

Resources

Stars

3 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages