tom-weatherhead/common-utilities.ts

View on GitHub
src/objects.ts

Summary

Maintainability
A
0 mins
Test Coverage
// github:tom-weatherhead/common-utilities.ts/src/objects.ts

// error TS2538: Type 'symbol' cannot be used as an index type.
// export type ObjectKeyType = number | string | symbol;
export type ObjectKeyType = number | string;
export type ObjectType<T> = Record<ObjectKeyType, T>;
export type DefaultObjectType = ObjectType<unknown>;

export function clone<T>(arg: T): T {
    // For an in-depth discussion of object copying, see https://scotch.io/bar-talk/copying-objects-in-javascript

    // **** Warning: JSON.parse(JSON.stringify(arg)) will fail for circular objects.
    // ? Do we need isCircular(obj) ?
    return JSON.parse(JSON.stringify(arg));

    // From avoidwork/haro/src/utility.js :
    // return JSON.parse(JSON.stringify(arg, null, 0)); // TODO: What are the second and third parameters to stringify() ?
}

export function copySpecifiedObjectProperties<T>(
    propertyList: string[],
    src: ObjectType<T>,
    dst: ObjectType<T> = {}
): ObjectType<T> {
    for (const property of propertyList.filter((p: string) => typeof src[p] !== 'undefined')) {
        dst[property] = src[property];
    }

    return dst;
}

export function combineObjects<T>(...objects: ObjectType<T>[]): ObjectType<T> {
    const combinedObject: ObjectType<T> = {};

    for (const object of objects) {
        for (const key of Object.keys(object)) {
            combinedObject[key] = object[key];
        }
    }

    return combinedObject;
}

export function getOwnProperties(obj: DefaultObjectType = {}): ObjectKeyType[] {
    /*
    // Version 1
    // See https://stackoverflow.com/questions/208016/how-to-list-the-properties-of-a-javascript-object
    let result = [];

    for (var key in obj) {
        if (obj.hasOwnProperty(key)) {
            result.push(key);
        }
    }

    return result;
    */

    // Version 2
    // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyNames
    return Object.getOwnPropertyNames(obj);
}

// E.g. getProperty(obj, 'subObj1.subObj2.arrayMember.length', 'Toast');

export function getProperty<T>(
    obj: DefaultObjectType,
    propertyPath: string,
    defaultValue: T
): T {
    const arrayOfProperties = propertyPath.split('.');
    let result: unknown = obj;

    for (const property of arrayOfProperties) {
        const tempObj = result as DefaultObjectType;

        // if (!isDefined(obj)) {
        if (typeof tempObj === 'undefined') {
            return defaultValue;
        }

        result = tempObj[property];
    }

    const castedResult = result as T;

    if (typeof castedResult === 'undefined') {
        return defaultValue;
    }

    return castedResult;
}

export function deleteUndefinedValuesFromObject<T>(obj: ObjectType<T>): ObjectType<T> {
    const keysToDelete = Object.keys(obj).filter((key) => typeof obj[key] === 'undefined');

    // keysToDelete.for Each((key) => {
    for (const key of keysToDelete) {
        delete obj[key];
    }
    // });

    return obj;
}

// export function followPropertyNamePath(obj: any = {}, propertyNames: string[] = [], defaultValue: any): any {

//     for (var i = 0; i < propertyNames.length; i++) {

//         if (!isObject(obj)) {
//             return defaultValue;
//         }

//         obj = obj[propertyNames[i]];
//     }

//     if (!isDefined(obj)) {
//         return defaultValue;
//     }

//     return obj;
// }

// export function overwriteSomeProperties (obj1, obj2) {
//     let result = clone(obj1);
//     const obj2OwnProperties = getOwnProperties(obj2);

//     obj2OwnProperties.for Each(prop => {
//         // TODO: Do nothing here if obj2[prop] is undefined.

//         if (!isAggregateEntity(obj2[prop])) {
//             // If result and obj2 are both arrays, do not allow result to be shortened.

//             if (prop !== 'length' || !isNumber(result.length) || !isNumber(obj2.length) || result.length < obj2.length) {
//                 result[prop] = obj2[prop];
//             }
//         } else if (!areTypesEqual(result[prop], obj2[prop])) {
//             result[prop] = clone(obj2[prop]);
//         } else {
//             result[prop] = overwriteSomeProperties(result[prop], obj2[prop]);
//         }
//     });

//     return result;
// }

// export function overwriteSomePropertiesAlt1 (obj1, obj2) {
//     return getOwnProperties(obj2).reduce(
//         (accumulator, element) => { // element is the name of a property in obj2
//             // TODO: Do nothing here if obj2[prop] is undefined.

//             if (!isAggregateEntity(obj2[element])) {
//                 // If accumulator and obj2 are both arrays, do not allow accumulator to be shortened.

//                 if (element !== 'length' || !isNumber(accumulator.length) || !isNumber(obj2.length) || result.length < obj2.length) {
//                     accumulator[element] = obj2[element];
//                 }
//             } else if (!areTypesEqual(accumulator[element], obj2[element])) {
//                 accumulator[element] = clone(obj2[element]);
//             } else {
//                 accumulator[element] = overwriteSomeProperties(accumulator[element], obj2[element]);
//             }

//             return accumulator;
//         },
//         clone(obj1)
//     );
// }

// export function containsCircularReference (obj, objsSeen = []) {

//     if (objsSeen.find(o => o === obj)) {
//         return true;
//     }

//     objsSeen.push(obj);

//     const result = Object.values(obj).some(subobj => containsCircularReference(subobj, objsSeen));

//     objsSeen.pop();

//     return result;
// }

// ****

// From https://stackoverflow.com/questions/201183/how-to-determine-equality-for-two-javascript-objects :

// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
export function deepEquals(x: any, y: any): boolean {
    const ok = Object.keys;
    const tx = typeof x;
    const ty = typeof y;

    return x && y && tx === 'object' && tx === ty && x.constructor === y.constructor // Fixes the error where deepEquals({}, []) was returning true.
        ? ok(x).length === ok(y).length && ok(x).every((key) => deepEquals(x[key], y[key]))
        : x === y;
}

// Some comments:

// yes, if you care for such corner case, ugly solution is to replace : (x === y) with : (x === y && (x != null && y != null  || x.constructor === y.constructor)) – atmin Jul 16 '18 at 7:42

// Even when replacing (x === y) with (x === y && (x != null && y != null || x.constructor === y.constructor)), the function(v2) still returns true(in NodeJS) when comparing {} and []. – derekbaker783 Aug 7 '19 at 16:37

// : (x === y) is not the place to try to fix the {} vs [] comparison. Instead put && a.constructor === b.constructor at the end of the main condition (i.e. before the ?). – James Clark Nov 24 '19 at 2:52