Lodash `set` in raw JavaScript

2022-03-17

/**
 * This is a replacement for lodash _.set(), it contains core functionality but
 * perhaps skips some edge cases.
 */
export const set = <T extends Record<string, unknown> = Record<string, unknown>>(
	obj: Record<keyof T, unknown>,
	path: string | string[],
	value: unknown
): T => {
	// Contains all characters that are not `.`, `[`, or `]`
	const validPathCharactersRegex = /([^[.\]])+/g;
	const pathArray = Array.isArray(path) ? path : path.match(validPathCharactersRegex);

	if (!pathArray) return obj;

	let nestedObj = obj;
	for (let i = 0; i < pathArray.length; i++) {
		let key: string | number = pathArray[i];

		// First, set the item to an object/array, if it's not the last we want
		// to make a nested path.
		if (nestedObj[key] === undefined) {
			const isIndex = isNaN(Number(pathArray[i + 1]));
			nestedObj[key as keyof T] = isIndex ? {} : [];
		}

		// If it's the last in the list, set the item to the value
		if (i === pathArray.length - 1) {
			nestedObj[key as keyof T] = value;
		}

		nestedObj = nestedObj[key];
	}

	return obj;
};

Context

set is a drop-in replacement for Lodash's _.set method. It allows you to set the value at a given path. If the path doesn't exist yet, it also creates the path for you! (woohoo, no undefined exceptions!).

I find many of my projects only use a few Lodash methods. Sometimes it's just better to just copy them over into the codbase "from scratch" (and save a dependency in the process!). While this set method doesn't cover all the edge cases Lodash does, it covers the primary workflows.

This snippet of set also is ~200 bytes, so it's super lightweight 😅

Usage

import { set } from './set';

const obj = { a: 1 };

// set a path deep in the object
set(obj, 'b.c.d', 3);
console.log(obj.b.c.d); // 3

// you can also set array items
set(obj, 'b.e[0].f', 4);
console.log(obj.b.e); // [{ f: 4 }]

Tests

import { set } from './set';

test('should allow setting nested paths', () => {
	const obj = {};
	set(obj, 'a.b.c', 1);
	// @ts-expect-error
	expect(obj.a.b.c).toEqual(1);
});

test('should allow setting nested array paths', () => {
	const obj = {};
	set(obj, 'a.b[0].c', 1);
	set(obj, 'a.b[1].c', 2);
	// @ts-expect-error
	expect(obj.a.b).toEqual([{ c: 1 }, { c: 2 }]);
});