@stnekroman/tstools
Version:
Set of handy tools for TypeScript development
249 lines (219 loc) • 7.96 kB
text/typescript
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;
}
}