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

typescript
import { registerLocaleData } from '@angular/common';
import localeFr from '@angular/common/locales/fr';
import localeFrExtra from '@angular/common/locales/extra/fr';
import localeDe from '@angular/common/locales/de';

Locale-aware bootstrapping in Angular

i18n localization dependency-injection
Intermediate 8 steps
typescript
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import * as Joi from 'joi';
 

Validating env config at boot in NestJS

configuration schema-validation environment-variables
Intermediate 8 steps
javascript
function evaluate(expression) {
  const tokens = tokenize(expression);
  let pos = 0;
 

Building a recursive descent calculator

parsing recursion operator-precedence
Intermediate 8 steps
typescript
import { Inject, Injectable, Logger } from '@nestjs/common';
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { Cache } from 'cache-manager';
import { InjectRepository } from '@nestjs/typeorm';

A cache-aside country lookup in NestJS

cache-aside dependency-injection batch-lookup
Intermediate 8 steps
typescript
import { Injectable, effect, signal, computed } from '@angular/core';
 
interface Preferences {
  theme: 'light' | 'dark';

A signal-based preferences store in Angular

signals state-management persistence
Intermediate 7 steps
typescript
import { useEffect, useState } from "react";
 
interface Section {
  id: string;

Building a scroll-spy hook in React

custom-hooks intersectionobserver dom-observation
Intermediate 8 steps

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