immerjs / immer

Create the next immutable state by mutating the current one

Home Page:https://immerjs.github.io/immer/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

How to correctly type a state query function?

JustASquid opened this issue Β· comments

πŸ™‹β€β™‚ Question

I have asked this question on StackOverflow, but didn't get much of a response, so I thought I'd try here.

Consider the following Typescript code:

import produce, {castDraft} from "immer";

interface Item {
  readonly id: number;
  readonly value?: string;
}

interface State {
  readonly items: Item[];
}

function findItemById(state: State, id: number): Item {
  return state.items.find(x => x.id === id)!;
}

const state: State = {
  items: [{ id: 1 }, { id: 2 }, { id: 3 }]
};

// This does not work.
produce(state, draft => {
  const item = findItemById(draft, 2);
  // Cannot assign to "value" because it is a read-only property.
  item.value = "Something";
});

// This works, but it's not ideal
produce(state, draft => {
  const item = castDraft(findItemById(draft, 2));
  item.value = "Something";
});

We're following the Immer.js recommendations of setting our State as readonly. We have a 'query function', which is used to pull a chunk out of the state. In this example, that's findItemById(). We quickly run into a problem, where whenever we grab the chunk of state from our draft instance in produce(), it loses the mutability wrapper and we cannot assign to it. We can use the function castDraft() to work around this issue, but I find that less than ideal - we will be using these kind of query functions a lot in our producers, so needing to cast all the time is problematic and error prone (if you accidentally cast from the external state and not the draft, you're in trouble!)

What we'd like to do is to define the type of the function findItemById() in such a way that it returns a writeable Item when the input State is writeable, otherwise returns an immutable item. I tried something like the following:

function findItemById<T extends Draft<State> | State>(
  state: T,
  id: number
): T extends Draft<State> ? Draft<Item> : Item {
  return state.items.find((x) => x.id === id)!;
}

But that didn't seem to work - the result was always writeable, even when the input was a regular State type. Is there any way to type the function to get the results I want?

Sorry never replied to this one I see. If this is still a question let me know. Otherwise closing.