Skip to content

Commit 199d2e5

Browse files
feat: fast deep object diff with TypeScript types
- diff() returns typed CREATE/UPDATE/DELETE changes with paths - applyDiff() applies changes immutably (structuredClone) - Handles objects, arrays, Dates, RegExps, NaN - 28 tests, zero dependencies, ~1.4KB Co-authored-by: Cursor <cursoragent@cursor.com>
0 parents  commit 199d2e5

File tree

17 files changed

+3992
-0
lines changed

17 files changed

+3992
-0
lines changed

.github/FUNDING.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
github: ofershap

.github/workflows/ci.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name: CI
2+
on:
3+
push:
4+
branches: [main]
5+
pull_request:
6+
branches: [main]
7+
jobs:
8+
test:
9+
runs-on: ubuntu-latest
10+
strategy:
11+
matrix:
12+
node-version: [20, 22]
13+
steps:
14+
- uses: actions/checkout@v4
15+
- uses: actions/setup-node@v4
16+
with:
17+
node-version: ${{ matrix.node-version }}
18+
- run: npm ci
19+
- run: npm run typecheck
20+
- run: npm test
21+
- run: npm run build
22+
- run: npm run lint

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
node_modules/
2+
dist/
3+
coverage/
4+
*.tsbuildinfo

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Ofer Shapira
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# deep-diff-ts
2+
3+
[![npm version](https://img.shields.io/npm/v/deep-diff-ts)](https://www.npmjs.com/package/deep-diff-ts)
4+
[![npm downloads](https://img.shields.io/npm/dm/deep-diff-ts)](https://www.npmjs.com/package/deep-diff-ts)
5+
[![license](https://img.shields.io/npm/l/deep-diff-ts)](https://github.com/ofershap/deep-diff-ts/blob/main/LICENSE)
6+
[![CI](https://github.com/ofershap/deep-diff-ts/actions/workflows/ci.yml/badge.svg)](https://github.com/ofershap/deep-diff-ts/actions/workflows/ci.yml)
7+
8+
Fast deep object diff with full TypeScript types. Zero dependencies. ~1.4KB.
9+
10+
![Demo](assets/demo.gif)
11+
12+
## Install
13+
14+
```bash
15+
npm install deep-diff-ts
16+
```
17+
18+
## Usage
19+
20+
```typescript
21+
import { diff } from "deep-diff-ts";
22+
23+
const oldObj = {
24+
name: "Alice",
25+
age: 30,
26+
tags: ["admin"],
27+
config: { theme: "dark" },
28+
};
29+
30+
const newObj = {
31+
name: "Alice",
32+
age: 31,
33+
tags: ["admin", "editor"],
34+
config: { theme: "light" },
35+
};
36+
37+
const changes = diff(oldObj, newObj);
38+
// [
39+
// { type: "UPDATE", path: ["age"], oldValue: 30, value: 31 },
40+
// { type: "CREATE", path: ["tags", 1], value: "editor" },
41+
// { type: "UPDATE", path: ["config", "theme"], oldValue: "dark", value: "light" }
42+
// ]
43+
```
44+
45+
### Apply Diffs
46+
47+
```typescript
48+
import { diff, applyDiff } from "deep-diff-ts";
49+
50+
const changes = diff(oldObj, newObj);
51+
const result = applyDiff(oldObj, changes);
52+
// result deeply equals newObj
53+
// oldObj is not mutated
54+
```
55+
56+
## Diff Types
57+
58+
Each difference has a `type` and `path`:
59+
60+
| Type | Fields | Description |
61+
| -------- | --------------------------- | ---------------------- |
62+
| `CREATE` | `path`, `value` | Property was added |
63+
| `UPDATE` | `path`, `oldValue`, `value` | Property value changed |
64+
| `DELETE` | `path`, `oldValue` | Property was removed |
65+
66+
`path` is an array of keys/indices: `["users", 0, "name"]`
67+
68+
## Supported Types
69+
70+
- Objects (deep recursive)
71+
- Arrays (element-by-element)
72+
- Dates (compared by time value)
73+
- RegExps (compared by string representation)
74+
- Primitives (string, number, boolean, null, undefined)
75+
- NaN (correctly handled via `Object.is`)
76+
77+
## API
78+
79+
### `diff(oldObj, newObj)`
80+
81+
Returns `Difference[]` describing all changes from `oldObj` to `newObj`.
82+
83+
### `applyDiff(target, diffs)`
84+
85+
Returns a new object with all diffs applied. Does not mutate the original.
86+
87+
## Other Projects
88+
89+
- [ts-result](https://github.com/ofershap/ts-result) — Rust-style Result<T, E> for TypeScript
90+
- [ts-nano-event](https://github.com/ofershap/ts-nano-event) — Typed event emitter in <200 bytes
91+
- [hover-effects](https://github.com/ofershap/hover-effects) — Canvas-based hover effects for images
92+
93+
## License
94+
95+
MIT

assets/demo.gif

1.55 MB
Loading

eslint.config.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import tseslint from "typescript-eslint";
2+
3+
export default tseslint.config(
4+
{ ignores: ["dist/", "node_modules/", "coverage/"] },
5+
...tseslint.configs.recommended,
6+
);

0 commit comments

Comments
 (0)