Is there a way to add TypeScript type safety to Lodash's set function?
I need to set values on deeply nested objects. The properties can be undefined, and I don't want to override other object properties.
This code works:
import set from "lodash/set";
type Res = {
position?: {
top?: { value: number },
right?: { value: number }
},
color?: {
red?: number,
blue?: number
}
};
const res : Res = {};
const items = ["p-top-1", "p-right-2", 'c-red-300'];
for (let i = 0; i < items.length; i++) {
const item = items[i];
const [type, key, value] = item.split('-');
if(type === 'p') {
set(res, `position.${key}.value`, value);
}
if(type === 'c') {
set(res, `color.${key}.value`, value);
}
}
But it doens't have type safety, eg this won't error:
if(type === 'p') {
set(res, `whatever-key-here.${key}.value`, value);
}
There is alike function set
in moderndash
, but it's missing a typesafe set-existing-value
counterpart
(it works exactly like lodash
's set
, so you may freely import { set as setUntyped } from "lodash"
instead)
By modifying its types to be a-la-set
rather then a-la-assign
, it's straightforward how to implement a typesafe version of set
:
import { set as setUntyped } from "moderndash";
import type { Call, Objects, Strings } from "hotscript";
import type { PlainObject } from "moderndash";
export function set<TObj extends PlainObject, TPath extends Call<Objects.AllPaths, TObj>>(
obj: TObj, path: TPath, value: Call<Objects.Get<TPath>, TObj>
): TObj {
return setUntyped(obj, path, value) as TObj
}
To use it with your splitted string you will also need a string.split
overload:
declare global {
interface String {
// typesafe 's-t-r-i-n-g'.split('-'): ['s', 't', 'r', 'i', 'n', 'g']
split<S extends string, D extends string>(this: S, separator: D): Call<Strings.Split<D>, S>;
}
}
Then the example code shows errors, just as expected: Playground: https://tsplay.dev/Nn886W
const res: Res = {};
const items = ["p-top-1", "p-right-2", 'c-red-300'] as const;
for (let i = 0; i < items.length; i++) {
const item = items[i];
const [type, key, value] = item.split('-')
if (type === 'p') {
set(res, `position.${key}.value`, value);
// ^!
// Argument of type '"1" | "2"' is not assignable to parameter of type 'number | undefined'.
}
if (type === 'c') {
set(res, `color.${key}.value`, value);
// ^!
// Argument of type '"300"' is not assignable to parameter of type 'undefined'.(2345)
}
}