Function to set nested property by dot-path, like _.set
Your code looks quite reasonable, there are only a few minor things that stand out to me.

You're constructing a new array every time a recursive call is made. For example, if the property passed is foo.bar.baz.qux.quuz, you will be constructing the following arrays of properties:

['foo', 'bar', 'baz', 'qux', 'quuz'] // initial call to `set`
['bar', 'baz', 'qux', 'quuz'] // 2nd call (recursive)
['baz', 'qux', 'quuz'] // 3rd call (recursive)
['qux', 'quuz'] // 4th call (recursive)
['quuz'] // 5th call (recursive)


These are all entirely separate arrays - when you .slice, you create a new array, it's not just a reference to a particular subset of the old array's indicies. Admittedly, it's unreasonable for a property list to be large enough for this to be a problem, but it seems a bit inelegant for space complexity. If you want to keep going the recursive route, you could fix it by creating the array only once in the initial call, then passing along an index to access.

Since the property is actually expected to be an array of properties, it would make more sense to pluralize the variable name. Also, since you're worried about purity, if you want to make it a bit more functional, rather than reassigning the parameter (which is better avoided when possible), put the result into a different variable, maybe by using a default argument:



function set (object, propertyStr, value, properties = propertyStr.split('.'), i = 0) {
  if (i === properties.length - 1) {
    object[properties[i]] = value;
  } else {
    set(object[properties[i]], '', value, properties, i + 1);
  }
}

const object = {
  position: {
    x: 5
  }
};
set(object, 'position.x', 1);
console.log(object);




But note that the function is still fundamentally impure, because its purpose is to mutate an argument. (That's not a problem unless you want it to be one, just something to keep in mind when discussing purity)

I think the recursion aspect makes things a bit more confusing than they need to be. I'd prefer to use reduce to iterate over the properties and access the last object, then assign the value to the last property on that last object:



const object = {
  position: {
    nested: {
      x: 5
    }
  }
};


function set(object, propertyStr, value) {
  const properties = propertyStr.split('.');
  const lastProperty = properties.pop();
  const lastObject = properties.reduce((a, prop) =&gt; a[prop], object);
  lastObject[lastProperty] = value;
}

set(object, 'position.nested.x', 'with pop');
console.log(object);


// If you want to avoid the .pop mutation, then:
function setNoPop(object, propertyStr, value) {
  const properties = propertyStr.split('.');
  const lastObject = properties.slice(0, properties.length - 1)
    .reduce((a, prop) =&gt; a[prop], object);
  lastObject[properties[properties.length - 1]] = value;
}

setNoPop(object, 'position.nested.x', 'no pop');
console.log(object);




The code will currently throw an error if an intermediate property doesn't exist or, in strict mode, is not an object. If you want an error to be thrown in this situation, that's OK, but you might prefer for the function to return a boolean indicating success or failure.



const object = {
  position: {
    nested: {
      x: 5
    }
  }
};

const isObj = param =&gt; typeof param === 'object' &amp;&amp; param !== null;
function set(object, propertyStr, value) {
  const properties = propertyStr.split('.');
  const lastProperty = properties.pop();
  const lastObject = properties.reduce((a, prop) =&gt; isObj(a) ? a[prop] : null, object);
  if (isObj(lastObject)) {
    lastObject[lastProperty] = value;
    return true;
  } else {
    return false;
  }
}

const success1 = set(object, 'position.nested.x', 'with pop');
console.log(success1);
const success2 = set(object, 'position.doesNotExist.x', 'with pop');
console.log(success2);