set.js

const isObject = value => (typeof value === "object" || typeof value === "function") && value !== null;

/**
 * Updates the value at given path of given object. It mutates the object. If path is not found then objects are created
 * "on the way". If non-objects are found, they are replaced with new plain objects. If primitives are used as source
 * they are ignored and returned value is empty object with updated value at given path.
 *
 * @param {Object} source - source object to mutate
 * @param {string|Array<string>} path - path where value should be stored, written as dot-separated property names or
 * array with property names. Use Array when your keys includes dots.
 * @param {*} value - value to be set
 * @example set(object, "deep.property", value)
 * @example set(object, ["deep", "property"], value)
 * @example set({}, "deep[0].property", value)
 * // will create this structure:
 * { "deep[0]": { "property": value }}
 * @example set({}, "items.0", value)
 * // will create object, not array
 * { "items": { "0": value }}
 * @returns {Object} - given object or new object if source was primitive
 */
const set = (source, path, value) => {
    const pathParts = typeof path === "string" ? path.split(".") : path;
    const len = pathParts.length;

    const result = isObject(source) ? source : {};
    let current = result; // eslint-disable-line init-declarations
    for (let i = 0; i < len; i++) {
        const isLast = i === len - 1;
        const key = pathParts[i];
        if (isLast) {
            current[key] = value;
            return result;
        }
        if (!isObject(current[key])) {
            current[key] = {};
        }
        current = current[key];
    }
    return result;
};

export default set;