Search code examples
typescriptlodash

TypeScript type safe version of Lodash's Set?


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);
  }

Solution

  • 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)
        }
    }