/** * Removes any null values from an object in-place * * @example * ```ts * let test = {a: 0, b: false, c: null, d: 'abc'} * console.log(clean(test)); // Output: {a: 0, b: false, d: 'abc'} * ``` * * @param {T} obj Object reference that will be cleaned * @param undefinedOnly Ignore null values * @returns {Partial} Cleaned object */ export function clean(obj: T, undefinedOnly = false): Partial { if(obj == null) throw new Error("Cannot clean a NULL value"); if(Array.isArray(obj)) { obj = obj.filter(o => o != null); } else { Object.entries(obj).forEach(([key, value]) => { if((undefinedOnly && value === undefined) || (!undefinedOnly && value == null)) delete (obj)[key]; }); } return obj; } /** * Create a deep copy of an object (vs. a shallow copy of references) * * Should be replaced by `structuredClone` once released. * * @param {T} value Object to copy * @returns {T} Type */ export function deepCopy(value: T): T { return JSON.parse(JSON.stringify(value)); } /** * Get/set a property of an object using dot notation * * @example * ```ts * // Get a value * const name = dotNotation(person, 'firstName'); * const familyCarMake = dotNotation(family, 'cars[0].make'); * // Set a value * dotNotation(family, 'cars[0].make', 'toyota'); * ``` * * @type T Return type * @param {Object} obj source object to search * @param {string} prop property name (Dot notation & indexing allowed) * @param {any} set Set object property to value, omit to fetch value instead * @return {T} property value */ export function dotNotation(obj: any, prop: string, set: T): T; export function dotNotation(obj: any, prop: string): T | undefined; export function dotNotation(obj: any, prop: string, set?: T): T | undefined { if(obj == null || !prop) return undefined; // Split property string by '.' or [index] return prop.split(/[.[\]]/g).filter(prop => prop.length).reduce((obj, prop, i, arr) => { if(prop[0] == '"' || prop[0] == "'") prop = prop.slice(1, -1); // Take quotes out if(!obj?.hasOwnProperty(prop)) { if(set == undefined) return undefined; obj[prop] = {}; } if(set !== undefined && i == arr.length - 1) return obj[prop] = set; return obj[prop]; }, obj); } /** * Recursively flatten a nested object, while maintaining key structure. * * @example * ```ts * const car = {honda: {model: "Civic"}}; * console.log(flattenObj(car)); //Output {honda.model: "Civic"} * ``` * * @param obj - Object to flatten * @param parent - Recursively check if key is a parent key or not * @param result - Result * @returns {object} - Flattened object */ export function flattenObj(obj: any, parent?: any, result: any = {}) { if(typeof obj === "object" && !Array.isArray(obj)) { for(const key of Object.keys(obj)) { const propName = parent ? parent + '.' + key : key; if(typeof obj[key] === 'object') { flattenObj(obj[key], propName, result); } else { result[propName] = obj[key]; } } return result; } } /** * Check that an object has the following values * * @example * ```ts * const test = {a: 2, b: 2}; * includes(test, {a: 1}); // true * includes(test, {b: 1, c: 3}); // false * ``` * * @param target Object to search * @param values Criteria to check against * @param allowMissing Only check the keys that are available on the target * @returns {boolean} Does target include all the values */ export function includes(target: any, values: any, allowMissing = false): boolean { if(target == undefined) return allowMissing; if(Array.isArray(values)) return values.findIndex((e: any, i: number) => !includes(target[i], values[i], allowMissing)) == -1; const type = typeof values; if(type != typeof target) return false; if(type == 'object') { return Object.keys(values).find(key => !includes(target[key], values[key], allowMissing)) == null; } if(type == 'function') return target.toString() == values.toString(); return target == values; } /** * Deep check if two objects are equal * * @param {any} a - first item to compare * @param {any} b - second item to compare * @returns {boolean} True if they match */ export function isEqual(a: any, b: any): boolean { const ta = typeof a, tb = typeof b; if((ta != 'object' || a == null) || (tb != 'object' || b == null)) return ta == 'function' && tb == 'function' ? a.toString() == b.toString() : a === b; const keys = Object.keys(a); if(keys.length != Object.keys(b).length) return false; return Object.keys(a).every(key => isEqual(a[key], b[key])); } export function mixin(target: any, constructors: any[]) { constructors.forEach(c => { Object.getOwnPropertyNames(c.prototype).forEach((name) => { Object.defineProperty( target.prototype, name, Object.getOwnPropertyDescriptor(c.prototype, name) || Object.create(null) ); }); }); } export function sanitizedJSON(obj: any, space?: number) { let cache: any[] = []; return JSON.parse(JSON.stringify(obj, (key, value) => { if (typeof value === 'object' && value !== null) { if (cache.includes(value)) return; cache.push(value); } return value; }, space)); }