Skip to content

Instantly share code, notes, and snippets.

@kalgon
Last active February 3, 2024 22:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kalgon/0be031bdb5b6f01c89f84fb5983dc50f to your computer and use it in GitHub Desktop.
Save kalgon/0be031bdb5b6f01c89f84fb5983dc50f to your computer and use it in GitHub Desktop.
diff
type Model<T> = T extends number | string | boolean ? typeof Object
: T extends Array<infer E> ? [Model<E>]
: { [P in keyof T]?: Model<T[P]> };
type Status = 'modified' | 'unchanged' | 'removed' | 'added';
type Diff<T> = Exclude<T, undefined | null> extends number | string | boolean ? { status: Status, left: T, right: T }
: Exclude<T, undefined | null> extends Array<infer E> ? { status: Status, elements: Array<Diff<E>> }
: { status: Status, props: { [P in keyof T]: Diff<T[P]> }};
function combine<T>(diffs: Array<Diff<T>>): Status {
return diffs.map(diff => diff.status).reduce((left, right) => left === right ? left
: left === 'modified' || right === 'modified' ? 'modified'
: left === 'unchanged' ? right
: right === 'unchanged' ? left
: 'modified', 'unchanged');
}
function diff<T>(left: T, right: T, model: Model<T>, statusOverride?: 'removed' | 'added'): Diff<T> {
const status = statusOverride ?? ((left === undefined) != (right === undefined) ? right === undefined ? 'removed' : 'added' : undefined);
if (model === Object) {
return { status: status ?? (left === right ? 'unchanged' : 'modified'), left, right } as Diff<T>;
}
const leftAny: any = left;
const rightAny: any = right;
if (Array.isArray(model)) {
const elements = [...Array(Math.max(leftAny?.length ?? 0, rightAny?.length ?? 0)).keys()].map(i => diff(leftAny?.[i], rightAny?.[i], model[0], status));
return { status: status ?? combine(elements), elements } as Diff<T>;
}
const props = Object.fromEntries(Object.entries(model).map(([key, propModel]) => [key, diff(leftAny?.[key], rightAny?.[key], propModel, status)]));
return { status: status ?? combine(Object.values(props)), props } as Diff<T>;
}
// EXAMPLE
type Person = {
firstName: string;
lastName?: string;
age: number;
addresses?: Array<{
street: string;
city?: string;
country?: string;
}>;
};
const left: Person = {
age: 42,
firstName: 'john',
/*
addresses: [{
street: 'foo',
city: 'NY'
}]*/
};
const right: Person = {
age: 34,
firstName: 'john',
lastName: 'doe',
addresses: [{
street: 'bar'
}, {
street: 'baz'
}]
};
const personDiff = diff(left, right, {
age: Object,
firstName: Object,
addresses: [{
city: Object,
street: Object
}]
});
console.log(personDiff);
console.log(personDiff.props.addresses?.elements[0].props.street.status); // type-safe diff!
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment