# Safely setting object properties with dot notation strings in JavaScript

### What my function does:

Safely sets object properties with dot notation strings in JavaScript.

That is to say, it allows setting a nested property on an object using a string such as "a.b.c".

For example: set(obj, "a.b.c", value) would be equivalent to a.b.c = value

### Some notes on intended behavior

• When setting a property at a given path, if any portion of that path doesn't exist, it should be created
• When creating a path portion, if the next key in the path is NOT an integer, the current portion should be created as an object
• When creating a path portion, if the next key in the path is an integer, the current portion should be created as an array

### Why?

I'd like to be able to set deeply nested properties on an object in an environment where I can't be sure that all the properties in the path will always exist and I don't want to deal with the logic of checking for and setting each key manually in more than one place. This allows me to quickly set the needed value without caring if the keys were present previously or not.

/**
* Set the value for the given object for the given path
* where the path can be a nested key represented with dot notation
*
* @param {object} obj   The object on which to set the given value
* @param {string} path  The dot notation path to the nested property where the value should be set
* @param {mixed}  value The value that should be set
* @return {mixed}
*
*/
function set(obj, path, value) {
// protect against being something unexpected
obj = typeof obj === 'object' ? obj : {};
// split the path into and array if its not one already
var keys = Array.isArray(path) ? path : path.split('.');
// keep up with our current place in the object
// starting at the root object and drilling down
var curStep = obj;
// loop over the path parts one at a time
// but, dont iterate the last part,
for (var i = 0; i < keys.length - 1; i++) {
// get the current path part
var key = keys[i];

// if nothing exists for this key, make it an empty object or array
if (!curStep[key] && !Object.prototype.hasOwnProperty.call(curStep, key)){
// get the next key in the path, if its numeric, make this property an empty array
// otherwise, make it an empty object
var nextKey = keys[i+1];
var useArray = /^\+?(0|[1-9]\d*)$/.test(nextKey); curStep[key] = useArray ? [] : {}; } // update curStep to point to the new level curStep = curStep[key]; } // set the final key to our value var finalStep = keys[keys.length - 1]; curStep[finalStep] = value; }; /** Usage **/ console.log('setting non existant a.b.c to "some value":'); var test = {}; set(test, 'a.b.c', 'some value'); console.log(test); console.log('updating a.b.c to "some new value":'); set(test, 'a.b.c', 'some new value'); console.log(test); console.log('setting non existant a.b.0 to "some value":'); var test = {}; set(test, 'a.b.0', 'some value'); console.log(test); console.log('updating a.b.0 to "some new value":'); set(test, 'a.b.0', 'some new value'); console.log(test); • Just in case you're not aware of it, Dottie may have considered some of the issues raised below - github.com/mickhansen/dottie.js – iabw May 14 '18 at 15:03 ## 2 Answers # Dangerous There are so many caveats in this function I would really not allow it in any code base because of potencial unexpected , or misunderstood behaviour. Its just waiting to cause problems in completely unrelated code. ## Some points. • Some variables should be const they are keys, key, useArray, nextKey, finalStep • Don't see why you create a new variable curStep (with very odd name) to hold the current object. Just reuse obj • Avoid the name set as it is used as a JavaScript token eg create a setter { set val(blah) {...} } Maybe the name could be assignToPath • null is also of type "object" so it will pay to extend the test on the first line to include the null Would be best to throw • You currently return undefined. Maybe it would be more helpful to return the object you assigned the new property to. • Why use the long version !Object.prototype.hasOwnProperty.call(curStep, key) ?? when !curStep.hasOwnProperty(key) will do the same. • If a property is expressly set to undefined your function fails. const test = { a: undefined } assignToPath(test, "a.b", "foo" ); //throws but would expect test.a.b = "foo"  • Objects can be locked via various Object functions Object.freeze, Object.seal, Object.preventExtensions, Object.defineProperties and Object.defineProperty Your function ignores these settings. • If a property exists but is not an Object or Array you still attempt to add a property to it. const test = { a: "blah" } assignToPath(test, "a.b", "foo" ); // fails  • This is what I would consider a low level function and as such would be one of the few that need to throw errors rather than fail silently. It should throw at anything that is not as expected. For example if the object to assign a property to is an array, or an object const test = { a: [] } assignToPath(test, "a.b", "foo" ); // Maybe this should throw const test = { a: {} } assignToPath(test, "a.1", "foo" ); // This should throw as it is unclear what the result should be. // the case against the above const test = {} assignToPath(test, "a.1", "foo" ); // creates an array  ## Suggestions only. Adding a settings argument would allow for better control of the behaviour when things get a little ambiguous. assignToPath({a:[]}, "a.b", "bar", {arrayHaveProps : true}); assignToPath({a:{}}, "a.1", "bar", {allowObjIndex : true});  You can shorten the code if you use a while loop and shift the path as you go. // simple example of using while and shift rather than a for loop var obj = {a:{b:{c:"a"}}}; const path = "a.b.c".split("."); while(path.length > 1){ obj = obj[path.shift()]; } obj[path.shift()] = "b";  • I guess set({}, "hasOwnProperty", null) is a reason to use Object.prototype.hasOwnProperty May 14 '18 at 8:00 • or even Object.create(null).hasOwnProperty() May 14 '18 at 8:02 • @sineemore Your examples are not reason to use the indirect call. How about Object.prototype.hasOwnProperty =()=> {} Now what??? May 14 '18 at 9:13 • Those examples are reason to use the indirect call, because they come up all the time in the real world. Nobody overwrites Object.prototype.hasOwnProperty on purpose while expecting things to work. – Ry- Apr 20 '20 at 6:58 obj = typeof obj === 'object' ? obj : {};  When obj argument points to something other than object you will create one. Like in set("foo", "a.b.c", "Yay!"). I guess you want to return it at function end or it will be lost. Other notes: • /^\+?(0|[1-9]\d*)$/ allows + in integer key inputs
• you may use isNaN(+nextKey) instead of regular expression, but beware float or signed numbers
• finalStep is a bit misleading name. You may use key = keys[keys.length - 1] instead.