Created
April 30, 2020 08:16
-
-
Save dagstuan/b1c92ba8aec2cbd8749d8e3d1f7425fc to your computer and use it in GitHub Desktop.
Immutable interop utils
This file contains 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
import produce from 'immer'; | |
export function getIn<T>( | |
object: Record<string, unknown>, | |
keyOrKeyPath: string | string[] | number | symbol, | |
notSetValue: T, | |
): T; | |
export function getIn<T>(object: Array<unknown>, key: string | number): T; | |
export function getIn<T>( | |
object: Record<string, unknown>, | |
keyOrKeyPath: string | string[] | number | symbol, | |
): T | undefined; | |
export function getIn<T>( | |
object: Record<string, unknown> | Array<unknown>, | |
keyOrKeyPath: string | string[] | number | symbol, | |
notSetValue: T | undefined = undefined, | |
): T | undefined { | |
if (!object) { | |
throw Error('object was undefined'); | |
} | |
const keyPath = Array.isArray(keyOrKeyPath) ? keyOrKeyPath : [keyOrKeyPath]; | |
// Cache the current object | |
let current = object; | |
// For each item in the path, dig into the object | |
for (let i = 0; i < keyPath.length; i++) { | |
// eslint-disable-next-line @typescript-eslint/no-explicit-any | |
const next = (current as any)[keyPath[i]]; | |
// If the item isn't found, return the default (or null) | |
if (next === undefined || next === null) { | |
return notSetValue; | |
} | |
// Otherwise, update the current value | |
current = next as Record<string, unknown>; | |
} | |
return (current as unknown) as T; | |
} | |
export function isEmpty(object: object) { | |
if (Array.isArray(object)) { | |
return object.length === 0; | |
} | |
return Object.keys(object).length === 0; | |
} | |
// Does not mutate original | |
// returns new object. | |
export function setIn<T extends Record<string, unknown>>( | |
object: T, | |
keyPath: string[] | string, | |
value: unknown, | |
): T { | |
const keyPathArr = Array.isArray(keyPath) ? keyPath : [keyPath]; | |
return produce(object, (draft) => { | |
let level = 0; | |
// eslint-disable-next-line @typescript-eslint/no-explicit-any | |
keyPathArr.reduce((accDraft: Record<any, any>, currKey) => { | |
level++; | |
const currVal = accDraft[currKey]; | |
if (level === keyPathArr.length && accDraft[currKey] !== value) { | |
accDraft[currKey] = value; | |
} else if (currVal === undefined) { | |
accDraft[currKey] = {}; | |
} | |
return accDraft[currKey]; | |
}, draft); | |
}); | |
} | |
export function updateIn<T, TVal>( | |
object: T, | |
keyPath: string[], | |
setter: (draftValue: TVal) => void, | |
): T; | |
export function updateIn<T, TVal>( | |
object: T, | |
keyPath: string[], | |
notSetValue: TVal | undefined, | |
setter: (draftValue: TVal) => void, | |
): T; | |
export function updateIn<T extends Record<string, unknown>, TVal>( | |
object: T, | |
keyPath: string[], | |
notSetValueOrSetter: TVal | undefined | ((draftValue: TVal) => void), | |
setter?: (draftValue: TVal) => void, | |
): T { | |
return produce(object, (draft) => { | |
let level = 0; | |
// eslint-disable-next-line @typescript-eslint/no-explicit-any | |
keyPath.reduce((accDraft: Record<any, any>, currKey) => { | |
level++; | |
const currVal = accDraft[currKey] as Record<string, unknown>; | |
if (level === keyPath.length) { | |
if (notSetValueOrSetter instanceof Function && !setter) { | |
accDraft[currKey] = produce(accDraft[currKey], (updDraft: TVal) => | |
notSetValueOrSetter(updDraft), | |
); | |
} else { | |
if (!currVal) { | |
accDraft[currKey] = notSetValueOrSetter; | |
} | |
accDraft[currKey] = produce( | |
accDraft[currKey], | |
(updDraft: TVal) => setter && setter(updDraft), | |
); | |
} | |
} else if (currVal === undefined) { | |
accDraft[currKey] = {}; | |
} | |
return accDraft[currKey]; | |
}, draft); | |
}); | |
} | |
export function update<TVal>( | |
object: Record<string, unknown>, | |
key: string, | |
updater: (value: TVal) => unknown, | |
) { | |
if (!object[key]) return object; | |
return produce(object, (draft) => { | |
draft[key] = updater(draft[key] as TVal); | |
}); | |
} | |
export function arraysEqual( | |
a: Array<string | number>, | |
b: Array<string | number>, | |
ignoreOrder = true, | |
) { | |
if (a === b) return true; | |
if (a == null || b == null) return false; | |
if (a.length != b.length) return false; | |
const sortedA = ignoreOrder ? [...a].sort() : a; | |
const sortedB = ignoreOrder ? [...b].sort() : b; | |
for (let i = 0; i < sortedA.length; ++i) { | |
if (sortedA[i] !== sortedB[i]) return false; | |
} | |
return true; | |
} | |
export function findIn<T>( | |
array: Array<T>, | |
predicate: (value: T, key: number) => boolean, | |
notSetValue?: T, | |
): T | undefined; | |
export function findIn<T>( | |
object: object, | |
predicate: (value: T, key: string) => boolean, | |
notSetValue?: T, | |
): T | undefined; | |
export function findIn<T>( | |
object: Array<T> | object, | |
predicate: | |
| ((value: T, key: number) => boolean) | |
| ((value: T, key: string) => boolean), | |
notSetValue?: T, | |
): T | undefined { | |
if (!object) { | |
throw new Error('object was undefined'); | |
} | |
if (Array.isArray(object)) { | |
return ( | |
object.find((val, key) => | |
(predicate as (value: T, key: number) => boolean)(val, key), | |
) ?? notSetValue | |
); | |
} else { | |
const obj = Object.entries(object).find(([key, val]) => | |
(predicate as (value: T, key: string) => boolean)(val, key), | |
); | |
return obj ? obj[1] : notSetValue; | |
} | |
} | |
export function sortArray<T>( | |
array: Array<T>, | |
sortFunction: ((a: T, b: T) => number) | undefined = undefined, | |
) { | |
return [...array].sort(sortFunction); | |
} | |
export function intersection<T>(arrayA: Array<T>, arrayB: Array<T>) { | |
if (!arrayA || !arrayB) { | |
throw new Error('One of the arrays was undefined'); | |
} | |
const setA = new Set(arrayA); | |
const setB = new Set(arrayB); | |
return Array.from(new Set([...setA].filter((x) => setB.has(x)))); | |
} | |
export function sortBy<T, V>( | |
array: Array<T>, | |
comparatorValueMapper: (value: T) => V, | |
) { | |
return produce(array, (draft) => { | |
return [...draft].sort((a, b) => { | |
const aVal = comparatorValueMapper(a as T); | |
const bVal = comparatorValueMapper(b as T); | |
if (aVal < bVal) { | |
return -1; | |
} | |
if (aVal > bVal) { | |
return 1; | |
} | |
return 0; | |
}); | |
}); | |
} | |
export function uniqueElements<T>(arr: Array<T>) { | |
return Array.from(new Set(arr)); | |
} | |
export function isSuperset<T>(arrA: Array<T>, arrB: Array<T>) { | |
const setA = new Set(arrA); | |
const setB = new Set(arrB); | |
return [...setB].every((bVal) => setA.has(bVal)); | |
} | |
export function take<T>(array: Array<T>, num: number) { | |
return array.slice(0, num); | |
} | |
export function skip<T>(array: Array<T>, num: number) { | |
return array.slice(num, array.length); | |
} | |
export function skipAndTake<T>( | |
array: Array<T>, | |
numToSkip: number, | |
numToTake: number, | |
) { | |
return take(skip(array, numToSkip), numToTake); | |
} | |
export function hasKey(object: object, key: string | number) { | |
return Object.prototype.hasOwnProperty.call(object, key); | |
} | |
export function groupBy<T, G extends string | number>( | |
arr: Array<T>, | |
grouper: (value: T) => G, | |
): Record<G, T[]> { | |
return arr.reduce<Record<G, T[]>>((groups, curr) => { | |
const val = grouper(curr); | |
groups[val] = groups[val] ?? []; | |
groups[val].push(curr); | |
return groups; | |
}, {} as Record<G, T[]>); | |
} | |
export function zipAll<T>(...arrays: Array<T>[]) { | |
const length = Math.max(...arrays.map((a) => a.length)); | |
return Array.from({ length }, (_, index) => | |
arrays.map((array) => array[index]), | |
); | |
} | |
interface Omit { | |
<T extends object, K extends [...(keyof T)[]]>(obj: T, ...keys: K): { | |
[K2 in Exclude<keyof T, K[number]>]: T[K2]; | |
}; | |
} | |
export const omit: Omit = (obj, ...keys) => { | |
const ret = {} as { | |
[K in keyof typeof obj]: typeof obj[K]; | |
}; | |
let key: keyof typeof obj; | |
for (key in obj) { | |
if (!keys.includes(key)) { | |
ret[key] = obj[key]; | |
} | |
} | |
return ret; | |
}; |
This file contains 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
import { | |
setIn, | |
updateIn, | |
update, | |
getIn, | |
arraysEqual, | |
findIn, | |
sortBy, | |
intersection, | |
uniqueElements, | |
isSuperset, | |
skip, | |
zipAll, | |
omit, | |
} from '../immutableInteropUtils'; | |
describe('immutableInteropUtils', () => { | |
describe('getIn', () => { | |
it('should get single property from object', () => { | |
const obj = { | |
foo: 2, | |
}; | |
const ret = getIn(obj, 'foo'); | |
expect(ret).toBe(2); | |
}); | |
it('should get path from object', () => { | |
const obj = { | |
foo: { | |
bar: { | |
baz: 2, | |
}, | |
}, | |
}; | |
const ret = getIn(obj, ['foo', 'bar', 'baz']); | |
expect(ret).toBe(2); | |
}); | |
it('should return undefined if a part of the path does not exist', () => { | |
const obj = { | |
foo: {}, | |
}; | |
const ret = getIn(obj, ['foo', 'bar', 'baz']); | |
expect(ret).toBe(undefined); | |
}); | |
it('should return default value if a part of the path does not exist', () => { | |
const obj = { | |
foo: {}, | |
}; | |
const ret = getIn(obj, ['foo', 'bar', 'baz'], 'default'); | |
expect(ret).toBe('default'); | |
}); | |
it('should return indices from arrays', () => { | |
const arrObj = [4, 3, 2, 1]; | |
const ret = getIn(arrObj, 3); | |
expect(ret).toBe(1); | |
}); | |
it('should return indices from arrays when specified as string', () => { | |
const arrObj = [4, 3, 2, 1]; | |
const ret = getIn(arrObj, '3'); | |
expect(ret).toBe(1); | |
}); | |
it('should return falsy values', () => { | |
const obj = { | |
foo: { | |
bar: { | |
baz: false, | |
}, | |
}, | |
}; | |
const ret = getIn(obj, ['foo', 'bar', 'baz']); | |
expect(ret).toBe(false); | |
}); | |
it('should return default value when part of the path is null', () => { | |
const obj = { | |
foo: { | |
bar: null, | |
}, | |
}; | |
const ret = getIn(obj, ['foo', 'bar', 'baz'], 'default'); | |
expect(ret).toBe('default'); | |
}); | |
it('should return default value when end result is null', () => { | |
const obj = { | |
foo: { | |
bar: { | |
baz: null, | |
}, | |
}, | |
}; | |
const ret = getIn(obj, ['foo', 'bar', 'baz'], 'default'); | |
expect(ret).toBe('default'); | |
}); | |
it('should not return default value when end result is falsy', () => { | |
const obj = { | |
foo: { | |
bar: { | |
baz: 0, | |
}, | |
}, | |
}; | |
const ret = getIn(obj, ['foo', 'bar', 'baz'], 'default'); | |
expect(ret).toBe(0); | |
}); | |
}); | |
describe('setIn', () => { | |
it('should set property in object', () => { | |
const keyPath = ['foo', 'bar']; | |
const object = {}; | |
const ret = setIn(object, keyPath, 'baz'); | |
expect(ret).toEqual({ | |
foo: { | |
bar: 'baz', | |
}, | |
}); | |
}); | |
it('should not overwrite existing values', () => { | |
const keyPath = ['foo', 'bar']; | |
const object = { | |
foo: { | |
bat: 2, | |
}, | |
}; | |
const ret = setIn(object, keyPath, 'baz'); | |
expect(ret).toEqual({ | |
foo: { | |
bat: 2, | |
bar: 'baz', | |
}, | |
}); | |
}); | |
it('should not mutate input', () => { | |
const keyPath = ['foo', 'bar']; | |
const object = { | |
foo: { | |
bat: 2, | |
}, | |
}; | |
const ret = setIn(object, keyPath, 'baz'); | |
expect(object).toEqual({ | |
foo: { | |
bat: 2, | |
}, | |
}); | |
expect(ret).toEqual({ | |
foo: { | |
bat: 2, | |
bar: 'baz', | |
}, | |
}); | |
expect(object).not.toEqual(ret); | |
}); | |
it('should set properties with string keypath input', () => { | |
const key = 'foo'; | |
const object = {}; | |
const ret = setIn(object, key, 'baz'); | |
expect(ret).toEqual({ | |
foo: 'baz', | |
}); | |
}); | |
it('should overwrite properties with string keypath input', () => { | |
const key = 'foo'; | |
const object = { | |
foo: 'lol', | |
}; | |
const ret = setIn(object, key, 'baz'); | |
expect(ret).toEqual({ | |
foo: 'baz', | |
}); | |
expect(object).toEqual({ | |
foo: 'lol', | |
}); | |
}); | |
it('should not return new object if nothing was changed', () => { | |
const object = { | |
currentPage: 1, | |
}; | |
const ret = setIn(object, 'currentPage', 1); | |
expect(ret).toEqual({ | |
currentPage: 1, | |
}); | |
expect(ret === object).toBe(true); | |
}); | |
}); | |
describe('updateIn', () => { | |
it('should update value', () => { | |
const keyPath = ['foo', 'bar']; | |
const object = { | |
foo: { | |
bar: 2, | |
}, | |
}; | |
const ret = updateIn(object, keyPath, (val: number) => val + 1); | |
expect(ret).toEqual({ | |
foo: { | |
bar: 3, | |
}, | |
}); | |
}); | |
it('should return same value if nothing is changed', () => { | |
const keyPath = ['foo', 'bar']; | |
const object = { | |
foo: { | |
bar: 2, | |
}, | |
}; | |
const ret = updateIn(object, keyPath, (val) => val); | |
expect(ret).toEqual({ | |
foo: { | |
bar: 2, | |
}, | |
}); | |
expect(object === ret).toBe(true); | |
}); | |
it('should set value even if path does not exist', () => { | |
const keyPath = ['foo', 'bar']; | |
const object = {}; | |
const ret = updateIn(object, keyPath, () => 1); | |
expect(ret).toEqual({ | |
foo: { | |
bar: 1, | |
}, | |
}); | |
}); | |
it('should set value even if path does not exist with default value', () => { | |
const keyPath = ['foo', 'bar']; | |
const object = {}; | |
const ret = updateIn(object, keyPath, {}, (draft: object) => ({ | |
...draft, | |
baz: 2, | |
})); | |
expect(ret).toEqual({ | |
foo: { | |
bar: { | |
baz: 2, | |
}, | |
}, | |
}); | |
}); | |
it('should set set value to notSetValue', () => { | |
const keyPath = ['foo', 'bar']; | |
const object = {}; | |
const ret = updateIn(object, keyPath, 1, (val: number) => val + 1); | |
expect(ret).toEqual({ | |
foo: { | |
bar: 2, | |
}, | |
}); | |
}); | |
}); | |
describe('update', () => { | |
it('should update property', () => { | |
const object = { | |
foo: 2, | |
}; | |
const ret = update(object, 'foo', (val: number) => val + 1); | |
expect(ret).toEqual({ | |
foo: 3, | |
}); | |
}); | |
it('should not update property if it does not exist', () => { | |
const object = {}; | |
const ret = update(object, 'foo', () => 1); | |
expect(ret).toEqual({}); | |
}); | |
}); | |
describe('arraysEqual', () => { | |
it('should compare arrays', () => { | |
const a = [1, 2, 3]; | |
const b = [1, 2, 3]; | |
const equal = arraysEqual(a, b); | |
expect(equal).toBe(true); | |
}); | |
it('should return equal with different orders', () => { | |
const a = [1, 2, 3]; | |
const b = [1, 3, 2]; | |
const equal = arraysEqual(a, b); | |
expect(equal).toBe(true); | |
}); | |
it('should return not equal with different orders not ignoring order', () => { | |
const a = [1, 2, 3]; | |
const b = [1, 3, 2]; | |
const equal = arraysEqual(a, b, false); | |
expect(equal).toBe(false); | |
}); | |
it('should work with strings', () => { | |
const a = ['fewa', 'fewaa']; | |
const b = ['fewa', 'fewaa']; | |
const equal = arraysEqual(a, b); | |
expect(equal).toBe(true); | |
}); | |
it('should work with strings of equal values', () => { | |
const a = ['fewa', 'fewa']; | |
const b = ['fewa', 'fewa']; | |
const equal = arraysEqual(a, b); | |
expect(equal).toBe(true); | |
}); | |
}); | |
describe('findIn', () => { | |
it('should find by value in array', () => { | |
const arr = [1, 2, 3]; | |
const ret = findIn(arr, (val) => val === 3); | |
expect(ret).toBe(3); | |
}); | |
it('should find by key in array', () => { | |
const arr = [12, 13, 14]; | |
const ret = findIn(arr, (_, key) => key === 2); | |
expect(ret).toBe(14); | |
}); | |
it('should return undefined if predicate returns false', () => { | |
const arr = [12, 13, 14]; | |
const ret = findIn(arr, (_, key) => key === 4); | |
expect(ret).toBe(undefined); | |
}); | |
it('should return undefined if predicate returns false', () => { | |
const arr = [12, 13, 14]; | |
const ret = findIn(arr, (_, key) => key === 4); | |
expect(ret).toBe(undefined); | |
}); | |
it('find by value in object', () => { | |
const obj = { | |
foo: 2, | |
bar: 3, | |
baz: 4, | |
}; | |
const ret = findIn(obj, (val) => val === 3); | |
expect(ret).toEqual(3); | |
}); | |
it('find by key in object', () => { | |
const obj = { | |
foo: 2, | |
bar: 3, | |
baz: 4, | |
}; | |
const ret = findIn(obj, (_, key) => key === 'baz'); | |
expect(ret).toEqual(4); | |
}); | |
}); | |
describe('sortBy', () => { | |
it('should sort by number property', () => { | |
const arr = [{ a: 2 }, { a: 0 }, { a: 3 }]; | |
const ret = sortBy(arr, (val) => val.a); | |
expect(ret).toEqual([{ a: 0 }, { a: 2 }, { a: 3 }]); | |
}); | |
it('should sort by string property', () => { | |
const arr = [{ a: 'c' }, { a: 'b' }, { a: 'a' }]; | |
const ret = sortBy(arr, (val) => val.a); | |
expect(ret).toEqual([{ a: 'a' }, { a: 'b' }, { a: 'c' }]); | |
}); | |
it('should return a new array even if nothing was changed', () => { | |
const arr = [{ a: 'a' }, { a: 'b' }, { a: 'c' }]; | |
const ret = sortBy(arr, (val) => val.a); | |
expect(ret).toEqual([{ a: 'a' }, { a: 'b' }, { a: 'c' }]); | |
expect(ret === arr).toBe(false); | |
}); | |
}); | |
describe('intersection', () => { | |
it('should return values in both arrays', () => { | |
const a = [1, 2, 3, 4]; | |
const b = [4, 5, 6]; | |
const ret = intersection(a, b); | |
expect(ret).toEqual([4]); | |
}); | |
it('should return empty array if no similar values', () => { | |
const a = [1, 2, 3]; | |
const b = [4, 5, 6]; | |
const ret = intersection(a, b); | |
expect(ret).toEqual([]); | |
}); | |
}); | |
describe('uniqueElements', () => { | |
it('should get unique elements in array of numbers', () => { | |
const arr = [1, 2, 3, 3, 4, 5, 6, 1, 1]; | |
const ret = uniqueElements(arr); | |
expect(ret).toEqual([1, 2, 3, 4, 5, 6]); | |
}); | |
it('should get unique elements in array of strings', () => { | |
const arr = ['a', 'b', 'b', 'a', 'c']; | |
const ret = uniqueElements(arr); | |
expect(ret).toEqual(['a', 'b', 'c']); | |
}); | |
}); | |
describe('isSuperset', () => { | |
it('should check if array is superset', () => { | |
const arrA = [1, 2, 3, 4]; | |
const arrB = [1, 2, 3]; | |
const aSupersetOfB = isSuperset(arrA, arrB); | |
const bSupersetOfA = isSuperset(arrB, arrA); | |
expect(aSupersetOfB).toBe(true); | |
expect(bSupersetOfA).toBe(false); | |
}); | |
}); | |
describe('skip', () => { | |
it('should skip first n elements in array', () => { | |
const arr = [1, 2, 3, 4, 5, 6, 7]; | |
const ret = skip(arr, 3); | |
expect(ret).toEqual([4, 5, 6, 7]); | |
}); | |
}); | |
describe('zipAll', () => { | |
it('should zip arrays of same length', () => { | |
const arrA = [1, 2, 3]; | |
const arrB = [4, 5, 6]; | |
const arrC = [7, 8, 9]; | |
const ret = zipAll(arrA, arrB, arrC); | |
expect(ret).toEqual([ | |
[1, 4, 7], | |
[2, 5, 8], | |
[3, 6, 9], | |
]); | |
}); | |
it('should zip arrays of different length', () => { | |
const arrA = [1, 2]; | |
const arrB = [3, 4, 5]; | |
const ret = zipAll(arrA, arrB); | |
expect(ret).toEqual([ | |
[1, 3], | |
[2, 4], | |
[undefined, 5], | |
]); | |
}); | |
it('should zip arrays of different length2', () => { | |
const arrA = [1, 2, 3]; | |
const arrB = [4, 5]; | |
const ret = zipAll(arrA, arrB); | |
expect(ret).toEqual([ | |
[1, 4], | |
[2, 5], | |
[3, undefined], | |
]); | |
}); | |
}); | |
describe('omit', () => { | |
it('should remove key from object', () => { | |
const initial = { | |
a: 1, | |
b: 2, | |
}; | |
const ret = omit(initial, 'b'); | |
expect(ret).toEqual({ | |
a: 1, | |
}); | |
}); | |
it('should not mutate original', () => { | |
const initial = { | |
a: 1, | |
b: 2, | |
}; | |
const ret = omit(initial, 'b'); | |
expect(initial).toEqual({ | |
a: 1, | |
b: 2, | |
}); | |
expect(ret).toEqual({ | |
a: 1, | |
}); | |
expect(ret === initial).toBe(false); | |
}); | |
}); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment