|
@@ -0,0 +1,127 @@
|
|
|
+type Obj = Record<string, any>;
|
|
|
+
|
|
|
+// Type guards
|
|
|
+
|
|
|
+export const isFunction = (something: unknown): something is CallableFunction => typeof something === 'function';
|
|
|
+
|
|
|
+export const isDefined = <T>(something: T | undefined): something is T => typeof something !== 'undefined';
|
|
|
+
|
|
|
+export const isNumber = (something: unknown): something is number => typeof something === 'number';
|
|
|
+
|
|
|
+export const isString = (something: unknown): something is string => typeof something === 'string';
|
|
|
+
|
|
|
+export const isRecord = (something: unknown): something is Obj => typeof something === 'object' && something !== null;
|
|
|
+
|
|
|
+export const whenDefined = <T, R>(something: T | undefined, fn: (something: T) => R): R | undefined => {
|
|
|
+ if (isDefined(something)) return fn(something);
|
|
|
+};
|
|
|
+
|
|
|
+// Type Casting:
|
|
|
+
|
|
|
+export const toNumber = (value: any): number => value?.toNumber?.() ?? (isNumber(value) ? value : NaN);
|
|
|
+
|
|
|
+// Math:
|
|
|
+
|
|
|
+export const clamp = (min: number, value: number, max: number) => Math.max(min, Math.min(max, value));
|
|
|
+
|
|
|
+// Objects:
|
|
|
+
|
|
|
+interface EqualsOption {
|
|
|
+ checkExtraKeys?: boolean;
|
|
|
+ depth?: boolean | number;
|
|
|
+}
|
|
|
+
|
|
|
+export const objectEquals = <T extends Obj>(
|
|
|
+ reference: T,
|
|
|
+ { checkExtraKeys = false, depth = 1 }: EqualsOption = {}
|
|
|
+): ((compared: T) => boolean) => {
|
|
|
+ const equalsOption = { checkExtraKeys, depth: isNumber(depth) ? depth - 1 : depth };
|
|
|
+ const expectedKeys: Array<keyof T> = Object.keys(reference);
|
|
|
+ return (compared) =>
|
|
|
+ (!checkExtraKeys || expectedKeys.length === Object.keys(compared).length) &&
|
|
|
+ expectedKeys.every((key) => equals(compared[key], equalsOption)(reference[key]));
|
|
|
+};
|
|
|
+
|
|
|
+export const equals = <T>(reference: T, { depth = 1, ...option }: EqualsOption = {}): ((compared: T) => boolean) => {
|
|
|
+ if (depth > 0 && isRecord(reference)) {
|
|
|
+ const isEqual = objectEquals(reference, { depth, ...option });
|
|
|
+ return (compared) => isRecord(compared) && isEqual(compared);
|
|
|
+ } else {
|
|
|
+ return (compared) => compared === reference;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+export const merge = <A extends Obj, B extends Obj = Partial<A>>(a: A, b: B): A & B => ({ ...a, ...b });
|
|
|
+
|
|
|
+export const propsEquals =
|
|
|
+ <T extends Obj>(...keys: (keyof T)[]) =>
|
|
|
+ (a: T, b: T) =>
|
|
|
+ keys.every((key) => a[key] === b[key]);
|
|
|
+
|
|
|
+export const definedValues = <T extends Record<any, any>>(obj: T): T =>
|
|
|
+ Object.fromEntries(Object.entries(obj).flatMap(([key, value]) => (isDefined(value) ? [[key, value]] : []))) as T;
|
|
|
+
|
|
|
+// Lists:
|
|
|
+
|
|
|
+export const dedupeObjects = <T extends Obj>(list: T[], options?: EqualsOption): T[] =>
|
|
|
+ list.reduce((remain: T[], item) => [...remain, ...(remain.some(objectEquals(item, options)) ? [] : [item])], []);
|
|
|
+
|
|
|
+export const intersperse = <T, S>(list: T[], toSeparator: (index: number, list: T[]) => S): (T | S)[] =>
|
|
|
+ list.length < 2 ? list : [list[0], ...list.slice(1).flatMap((item, index) => [toSeparator(index, list), item])];
|
|
|
+
|
|
|
+export const partition = <T>(list: T[], predicate: (x: T) => boolean): [T[], T[]] =>
|
|
|
+ list.reduce(
|
|
|
+ ([pass, fail]: [T[], T[]], item): [T[], T[]] =>
|
|
|
+ predicate(item) ? [[...pass, item], fail] : [pass, [...fail, item]],
|
|
|
+ [[], []]
|
|
|
+ );
|
|
|
+
|
|
|
+export const repeat = <T>(getItem: (index: number) => T, times: number): T[] =>
|
|
|
+ Array.from({ length: times }, (_, i) => getItem(i));
|
|
|
+
|
|
|
+export const debounce = <T extends (...params: any[]) => any>(fn: T, delay = 400) => {
|
|
|
+ type Result = (ReturnType<T> extends Promise<infer U> ? U : ReturnType<T>) | undefined;
|
|
|
+ let latestTimeout: ReturnType<typeof setTimeout> | undefined;
|
|
|
+
|
|
|
+ return (...params: Parameters<T>) =>
|
|
|
+ new Promise<Result>((resolve) => {
|
|
|
+ const resolveImmediately = !latestTimeout;
|
|
|
+
|
|
|
+ const timeout = setTimeout(() => {
|
|
|
+ if (timeout !== latestTimeout) {
|
|
|
+ !resolveImmediately && resolve(undefined);
|
|
|
+ } else {
|
|
|
+ latestTimeout = undefined;
|
|
|
+ !resolveImmediately && resolve(fn(...params));
|
|
|
+ }
|
|
|
+ }, delay);
|
|
|
+ latestTimeout = timeout;
|
|
|
+
|
|
|
+ resolveImmediately && resolve(fn(...params));
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+export const last = <T>(list: ArrayLike<T>): T => list[list.length - 1];
|
|
|
+export const flatten = <T>(nested: (T | T[])[]) => ([] as T[]).concat(...nested);
|
|
|
+
|
|
|
+export const asArray = <T>(item: undefined | T): T[] => (isDefined(item) ? [item] : []);
|
|
|
+
|
|
|
+type Item = Record<string, any>;
|
|
|
+
|
|
|
+export const arrayGroupBy = (items: Item[], key: keyof Item) =>
|
|
|
+ items.reduce(
|
|
|
+ (result, item) => ({
|
|
|
+ ...result,
|
|
|
+ [item[key]]: [...(result[item[key]] || []), item],
|
|
|
+ }),
|
|
|
+ {}
|
|
|
+ );
|
|
|
+
|
|
|
+// Promises:
|
|
|
+
|
|
|
+type MapperP<T, R> = (value: T, index: number, array: T[] | readonly T[]) => Promise<R>;
|
|
|
+export const mapP = <T, R>(list: T[] | readonly T[], mapper: MapperP<T, R>): Promise<R[]> =>
|
|
|
+ Promise.all(list.map(mapper));
|
|
|
+
|
|
|
+export const flatMapP = async <T, R>(list: T[] | readonly T[], mapper: MapperP<T, R | R[]>): Promise<R[]> =>
|
|
|
+ Promise.all(flatten(await mapP(list, mapper)));
|