Last active
February 27, 2023 05:30
-
-
Save Phryxia/b82ecc81f4f51c0fd2b5ed937d5347a1 to your computer and use it in GitHub Desktop.
TypeScript implementation of computing difference of given two JavaScript value
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| type Addition = { | |
| type: 'addition' | |
| next: any | |
| } | |
| type Change = { | |
| type: 'change' | |
| prev: any | |
| next: any | |
| } | |
| type Deletion = { | |
| type: 'deletion' | |
| prev: any | |
| } | |
| type Diff = Addition | Change | Deletion | undefined | Diff[] | { [key: string]: Diff } | |
| function diff(oPrev: any, oNext: any): Diff { | |
| // primitive | |
| if (typeof oPrev !== typeof oNext || typeof oPrev !== 'object') { | |
| if (oPrev === oNext) return undefined | |
| if (oPrev === undefined) { | |
| return { | |
| type: 'addition', | |
| next: oNext, | |
| } | |
| } | |
| if (oNext === undefined) { | |
| return { | |
| type: 'deletion', | |
| prev: oPrev, | |
| } | |
| } | |
| return { | |
| type: 'change', | |
| prev: oPrev, | |
| next: oNext, | |
| } | |
| } | |
| // both null | |
| if (!oPrev && !oNext) return undefined | |
| // only one is null | |
| if (!oPrev || !oNext) { | |
| return { | |
| type: 'change', | |
| prev: oPrev, | |
| next: oNext, | |
| } | |
| } | |
| const isPrevArray = oPrev instanceof Array | |
| const isNextArray = oNext instanceof Array | |
| if (isPrevArray && isNextArray) { | |
| // early termination | |
| if (oPrev === oNext) return undefined | |
| const maxLength = oPrev.length > oNext.length ? oPrev.length : oNext.length | |
| const minLength = oPrev.length > oNext.length ? oNext.length : oPrev.length | |
| const result = [] as any[] | |
| for (let i = 0; i < maxLength; ++i) { | |
| if (i < minLength) { | |
| result[i] = diff(oPrev[i], oNext[i]) | |
| } else if (oPrev.length === maxLength) { | |
| result[i] = { | |
| type: 'deletion', | |
| prev: oPrev[i], | |
| } | |
| } else { | |
| result[i] = { | |
| type: 'addition', | |
| next: oNext[i], | |
| } | |
| } | |
| } | |
| if (result.some(Boolean)) { | |
| return result | |
| } | |
| return undefined | |
| } | |
| if (!isPrevArray && !isNextArray) { | |
| // early termination | |
| if (oPrev === oNext) return undefined | |
| const prevKeys = Object.fromEntries(Object.keys(oPrev).map(key => [key, true])) | |
| const nextKeys = Object.fromEntries(Object.keys(oNext).map(key => [key, true])) | |
| const commKeys = { ...prevKeys, ...nextKeys } | |
| const result = {} as Record<string, any> | |
| for (const key of Object.keys(commKeys)) { | |
| if (prevKeys[key] && nextKeys[key]) { | |
| const difference = diff(oPrev[key], oNext[key]) | |
| if (difference) { | |
| result[key] = difference | |
| } | |
| } else if (prevKeys[key]) { | |
| result[key] = { | |
| type: 'deletion', | |
| prev: oPrev[key], | |
| } | |
| } else { | |
| result[key] = { | |
| type: 'addition', | |
| next: oNext[key] | |
| } | |
| } | |
| } | |
| if (Object.keys(result).length === 0) return undefined | |
| return result | |
| } | |
| return { | |
| type: 'change', | |
| prev: oPrev, | |
| next: oNext, | |
| } | |
| } |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This function computes the difference between two given object. The exact rules are pretty complicated, though it's intuitive.
Example
This function is inspired by
git diff. You can think this function asgit diffof JavaScriptobject. Any result for unchanged value isundefined.This function also supports array recursion like following:
This function also supports object recursion like following:
Rule
undefinedandnullare differentnullis considered as primitive valuefunctiontype is only considred with its referenceundefined→ nonundefinedis considered as additionundefined→undefinedis considered as deletionundefined→ nonundefinedis either change or sameArrayorobject(but notnull) will be compared recursivelyArrayoPrevis longer thanoNext, unmatched elements ofoPrevin [oNext.length, oPrev.length) will be considered as deletionoPrevis shorter thanoNext, unmatched elements ofoNextin [oPrev.length, oNext.length) will be considered as additionobjectoPrevwhich doesn't exist inoNextwill be considered as deletionoNextwhich doesn't exist inoPrevwill be considered as additionundefinedThe return type of given function is not possible to represented perfectly using typescript, since it's an inifnite set. TypeScript type infer system has limitation. It can be described by following context free-like grammar: (actually I don't have idean what is this structures is called)
Caveats
Note that this code doesn't protect infinite recursion caused by mutual dependency between two compound types. In general sense it's very useless. To protect this, very high cost operation is required.
Note that this function doesn't find maximum match for array. For example, following two arrays has 3 changes and one deletion. Finding maximum match is very high cost (See React's reconcillation docs)