typescriptgenericsmutabletypescript3.0

TypeScript: Recursive Deep Mutable with Generics. Error: T is not assignable to Mutable<T>


I'm trying to write a deep recursive Mutable type:

Mutable<T> = ...
    // remove all "readonly" modifiers
    // convert all ReadonlyArray to Array
    // etc.
    // and do it all recursively

const mutate = <T>(val: T, mutateFn: (mutableVal: Mutable<T>) => void): T => {
  return null as any
}

It's working fine as long as I don't use generics:

// works fine
mutate([] as ReadonlyArray<string>, mutableVal => {
  mutableVal.splice(42, 0, "test");
});

But when using it within a generic function I get an error:

// Error: Argument of type 'T' is not assignable to parameter of type 'Mutable<T>'.
const doSomething = <T>(array: ReadonlyArray<T>, index: number, elem: T) => {
  mutate(array, mutableVal => {
                      // Here's the error
                      //         v
    mutableVal.splice(index, 0, elem);
  });
}

I understand, that the mutable array's type is Array<Mutable<T>>, and that splice now expects a Mutable<T> value, instead of T. But I can't figure out how to solve it.

Do you have any idea how to solve this?

I've created a TypeScript Playground, so you can play with the code: Link to TypeScript Playground


Solution

  • My suggestion is to do something like this:

    const doSomething = <T>(array: ReadonlyArray<T>, index: number, elem: T) => {
        mutate({ array: array, elem: elem }, mutableVal => {
            mutableVal.array.splice(index, 0, mutableVal.elem);
        });
    }
    

    The idea is that you need elem to be mutable in order to add it to a deep-mutable array, but your original call was not doing that. Since you want to mutate both array and possibly elem, the most straightforward solution is to pass in an object containing both array and elem, and operate on the deep-mutable version of that.

    Only you know if it's acceptable to call mutate() on elem as well as on array, since the implementation of mutate() is left out. I'm guessing it's going to be something involving an assertion like this:

    const mutate = <T>(val: T, mutateFn: (mutableVal: Mutable<T>) => void): T => {
        mutateFn(val as Mutable<T>); //🤷‍♀️
        return val;
    }
    

    in which case I'd say "who cares" whether you call mutate() on elem, or whether you just assert elem to its mutable counterpart inside doSomething(). On the other hand, if your implementation is something fancier involving cloning, then you should think about whether it makes sense to call it on elem or not.

    Okay, hope that helps. Good luck!