UNPKG

@stnekroman/tstools

Version:

Set of handy tools for TypeScript development

249 lines (219 loc) 7.96 kB
import { Types } from './Types'; export namespace Objects { export function isNotNullOrUndefined<T>(arg: T | null | undefined): arg is NonNullable<T> { return arg !== null && arg !== undefined; } export function isNullOrUndefined<T>(arg: T | null | undefined): arg is null | undefined { return arg === null || arg === undefined; } export function isObject<T = {}>(arg: unknown | T): arg is Record<keyof T, T[keyof T]> { return typeof arg === 'object' && arg !== null; } export function isFunction(arg: unknown): arg is Function { return typeof arg === 'function'; } export function isArray<T>(arg: unknown): arg is T[] { return Array.isArray(arg) || arg instanceof Array; } export function isString(arg: unknown): arg is string { return typeof arg === 'string'; } export function isNumeric(arg: unknown): arg is number { if (!Objects.isNotNullOrUndefined(arg)) { return false; } return typeof arg === 'bigint' || (typeof arg === 'number' && !isNaN(arg as number)); } export function isBoolean(arg: unknown): arg is boolean { return typeof arg === 'boolean'; } export function isPrimitive(arg: unknown): arg is Types.Primitive { if (!Objects.isNotNullOrUndefined(arg)) { return true; } if (Objects.isObject(arg) || Objects.isFunction(arg)) { return false; } else { return true; } } export function forEach<T extends {}>(obj: T, callback: (key: keyof T, value: T[keyof T]) => void): void { for (const key of Object.keys(obj) as (keyof T)[]) { callback(key, obj[key]); } } /** * @param test object * @param targetClass target class (constructor) to test against * @returns true, of given test object is constructor of class, which extends target class (actually constructor) */ export function isConstructorOf<T>(test: unknown, targetClass: Types.Newable<T>): test is Types.Newable<T> { if (test === targetClass) { return true; } if (Objects.isFunction(test)) { let prototype = test.prototype; while (prototype) { if (prototype instanceof targetClass) { return true; } prototype = prototype.prototype; } } return false; } // port of this https://medium.com/@stheodorejohn/javascript-object-deep-equality-comparison-in-javascript-7aa227e889d4 export function equals<T1, T2>(obj1?: T1, obj2?: T2): boolean { if (obj1 === obj2) { return true; } else if (obj1 && obj2 && Objects.isObject(obj1) && Objects.isObject(obj2)) { const keys1: string[] = Object.keys(obj1); const keys2: Set<string> = new Set(Object.keys(obj2)); for (const key of keys1) { if (!Objects.equals(obj1[key as keyof T1], obj2[key as keyof T2])) { return false; } keys2.delete(key); } return keys2.size === 0; } else { return false; } } /** * Not an analog of structuredClone (which is more low-level copying for data serialization). * when @param transferNotCopyable set to `false` - it will perform like structuredClone, but not all fields types are covered and it won't create copies of strings - they will go by reference. When unable to clone - expection raised. * when @param transferNotCopyable set to `true` - will try to clone, if not cloneable - just a ref will be copies/transferred. */ export function deepCopy<T>(obj: Types.Serializable<T>): T; export function deepCopy<T>(obj: T, transferNotCopyable: true): T; export function deepCopy<T>(obj: T | Types.Serializable<T>, transferNotCopyable?: true): T { if (Objects.isPrimitive(obj)) { return obj; } if (obj instanceof Date) { const date: Date = new Date(); date.setTime(obj.getTime()); return date as T; } if (Array.isArray(obj)) { return obj.map((item) => deepCopy(item, transferNotCopyable!)) as T; } if (Objects.isObject(obj)) { const copy: T = {} as T; Objects.forEach(obj, (key: keyof T, value: T[keyof T]): void => { copy[key] = Objects.deepCopy(value, transferNotCopyable!); }); return copy; } if (transferNotCopyable) { return obj; } else { throw new Error('Cannot clone not-copyable fields (methods inside?) during Objects.deepCopy.'); } } /** * Backed by Objects.deepCopy(..., true), so you can pass obj with functions - they will be mapped to dst objection as is. * @param dst - target destination object to modify / extend * @param src - source of incomming changes * @returns true, if dst object was modified */ export function extend<DST extends {}, SRC extends {}>( dst: DST, src: SRC, options?: { canOverwrite?: <DST extends {}, SRC extends {}>(dst: DST, src: SRC, key: keyof SRC) => boolean; } ): boolean { const canOverwrite = options?.canOverwrite ?? (() => true); let changed = false; if (Objects.isNotNullOrUndefined(src)) { Objects.forEach(src, (key: keyof SRC | keyof DST, value: SRC[keyof SRC]): void => { if (Objects.isObject(value) && Objects.isObject(dst[key as keyof DST])) { // look deeper... changed ||= Objects.extend(dst[key as keyof DST] as {}, value); } else { const overwrite = canOverwrite(dst, src, key as keyof SRC); if (overwrite) { // no merge, just pick a copy dst[key as unknown as keyof typeof dst] = Objects.deepCopy( src[key as keyof SRC], true ) as unknown as (typeof dst)[keyof typeof dst]; } changed ||= overwrite; } }); } return changed; } // from https://stackoverflow.com/a/69330363 export function isCharacterWhitespace(c: string): boolean { return ( c === ' ' || c === '\n' || c === '\t' || c === '\r' || c === '\f' || c === '\v' || c === '\u00a0' || c === '\u1680' || c === '\u2000' || c === '\u200a' || c === '\u2028' || c === '\u2029' || c === '\u202f' || c === '\u205f' || c === '\u3000' || c === '\ufeff' ); } export function isBlankString(str: string): boolean { if (Objects.isNotNullOrUndefined(str)) { for (const ch of str) { if (!Objects.isCharacterWhitespace(ch)) { return false; } } } return true; } /** * Visitor pattern impl for tree data structure. * @param obj - object to visit (visits very first high level obj from biggining) * @param visitor your callback, which will accept objects one-by-one and level of nesting, returning children for further processing * `visitor` accepts object itself as first argument, level of nesting (root is 0) and may return array of children. If not returns (return undefined) - processing of deeper children levels stops. */ export function visit<OBJ extends {}>(obj: OBJ, visitor: (obj: OBJ, level: number) => OBJ[] | void): void { visitLevel(obj, visitor); } function visitLevel<OBJ extends {}>( obj: OBJ, visitor: (obj: OBJ, level: number) => OBJ[] | void, level: number = 0 ): void { const children = visitor(obj, level); if (children) { for (const child of children) { visitLevel(child, visitor, level + 1); } } } /** * Flattens tree data structure to array of references. * @param obj object to start flatten from. * @param childrenExtractor - function, which will provide children for each object individually * @returns array of references to */ export function flatten<OBJ extends {}>( obj: OBJ, childrenExtractor: (obj: OBJ, level: number) => OBJ[] | void ): OBJ[] { const array: OBJ[] = []; visit(obj, (obj, level) => { array.push(obj); return childrenExtractor(obj, level); }); return array; } }