typescript 35 lines · 9 steps

Building a recursive deep-diff in TypeScript

A recursive function that compares two nested objects and reports exactly what was added, removed, or changed.

Explained by highlit
1type Change =
2 | { kind: "added"; path: string; value: unknown }
3 | { kind: "removed"; path: string; value: unknown }
4 | { kind: "updated"; path: string; from: unknown; to: unknown };
5 
6function isRecord(value: unknown): value is Record<string, unknown> {
7 return typeof value === "object" && value !== null && !Array.isArray(value);
8}
9 
10export function deepDiff(before: unknown, after: unknown, base = ""): Change[] {
11 if (Object.is(before, after)) return [];
12 
13 if (!isRecord(before) || !isRecord(after)) {
14 return [{ kind: "updated", path: base, from: before, to: after }];
15 }
16 
17 const changes: Change[] = [];
18 const keys = new Set([...Object.keys(before), ...Object.keys(after)]);
19 
20 for (const key of keys) {
21 const path = base ? `${base}.${key}` : key;
22 const hasBefore = key in before;
23 const hasAfter = key in after;
24 
25 if (hasBefore && !hasAfter) {
26 changes.push({ kind: "removed", path, value: before[key] });
27 } else if (!hasBefore && hasAfter) {
28 changes.push({ kind: "added", path, value: after[key] });
29 } else {
30 changes.push(...deepDiff(before[key], after[key], path));
31 }
32 }
33 
34 return changes;
35}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1A discriminated union lets each result variant carry exactly the fields it needs, so consumers can switch safely on kind.
  2. 2Recursion naturally mirrors nested data — each level handles one object and delegates its children to the same function.
  3. 3Unioning both objects' keys ensures additions and removals surface, not just changes to shared keys.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Building a recursive deep-diff in TypeScript — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code