How to replace every key in a nested object given a mapping in JavaScript?

How to convert each and every nested object key when the structure and values remain the same. The key mapping is arbitrary, yet predetermined like:

x -> key1
y -> nested1
x.z -> nested1.key1
...

And in terms of a sample JavaScript object:

const inObj = {
  x: "A value",
  y: {
    z: "Another value"
  }
}

// ...

// Desired output
// {
//   key1: "A value",
//   nested1: {
//     key1: "Another value"
//   }
// }

Edit: I do not want to mutate the original object. By intuition, I thought of using an object for old and new keys and using map() function. However, I could not settle a logic for a nested and complex object with that.

Answer

My favorite workhorse for this type of thing is the following little function.

function transform(obj, fn) {

    function transformer(x) {
        if (!x || typeof x !== 'object')
            return x;

        if (Array.isArray(x))
            return x
                .map((v, k) => fn(k, v, transformer))
                .filter(x => x)
                .reduce((a, x) => (a[x[0]] = x[1], a), []);

        return Object.fromEntries(
            Object.entries(x)
                .map(([k, v]) => fn(k, v, transformer))
                .filter(x => x));
    }

    return transformer(obj);
}

How does this work? It walks your object recursively and invokes a callback for each key-value (or index-value) pair. The callback receives three arguments: a key, a value and a “carry on” function and is supposed to return a new key-value pair. In the callback you can choose to continue recursion by calling the “carry on” function on the value, but you don’t have to. If the callback returns undefined, the pair is eliminated.

Applied to your question, the callback might look like this:

REPLACE = {
    'x': 'key1',
    'y': 'nested1',
    'y.z': 'nested1.key1',
}

stack = [];

result = transform(inObj, (key, val, carryOn) => {
    stack.push(key)
    val = carryOn(val)

    let path = stack.join('.')
    if (path in REPLACE)
        key = REPLACE[path].split('.').pop()

    stack.pop()
    return [key, val]
})

You might want to wrap the whole thing into a function to avoid the global stack.