Search code examples
typescripttypescript-generics

Safely setting a nested optional attribute in Typescript


In Typescript, I need to set a nested field in a type that similar to this one:

type NestedOptional = {
  a?: { b?: { c?: { d?: number; }; }; };
};

This requires a lot of boilerplate code to initialize all the non-leaves objects when needing to set the d leaf value. How can I avoid this?

function fillLeafValue(obj: NestedOptional, value: number) {
  // how can I avoid this boilerplate code when setting a value,
  // as does the JS optional chaining (?.) for getting values?
  if (obj.a == undefined) obj.a = {};
  if (obj.a.b == undefined) obj.a.b = {};
  if (obj.a.b.c == undefined) obj.a.b.c = {};
  obj.a.b.c.d = value;
}

I tried something like this:

// *** does not work: issue with generics recursion?
function ensure<T>(obj: T) {
  return function <K extends keyof T>(key: K, ctor: () => T[K]) {
    if (obj[key] == undefined) obj[key] = ctor();
    return ensure(obj[key]);
  };
}

// I wish I could use it like the following

const empty = () => ({}); // how to init the objects

function fillLeafValue2(obj: NestedOptional, value: number) {
  ensure(obj)("a", empty)("b", empty)("c", empty);
  //                       ^ Argument of type 'string' is not assignable to parameter of type 'never'.
}

Also tried things like ts-get-set npm package but it's not strict enough: it allows setting "a.z.c" without an error for instance!

FWIW, I am actually using Immer JS with the produce method, but I still need to fill all optional layers of the draft.

Any advice? It looks like a pretty common case...


Solution

  • It's as simple as that (no proxy, no lib...):

    (((obj.a ??= {}).b ??= {}).c ??= {}).d = value;
    

    I am eventually providing the answer to my own question because after 4 months, neither @VLAZ nor @jcalz have added an answer that I could officially accept. Thanks to both of them for providing me the answer in the question comments 🙏 All credit to them!