Skip to content

Instantly share code, notes, and snippets.

@dagstuan
Created April 30, 2020 08:16
Show Gist options
  • Save dagstuan/b1c92ba8aec2cbd8749d8e3d1f7425fc to your computer and use it in GitHub Desktop.
Save dagstuan/b1c92ba8aec2cbd8749d8e3d1f7425fc to your computer and use it in GitHub Desktop.
Immutable interop utils
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;
};
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