menu

mobx-state-tree

Opinionated, transactional, MobX powered state container combining the best features of the immutable and mutable world for an optimal DX

Channels
Team

Generic set action in Typescript

July 21, 2019 at 7:11pm

Generic set action in Typescript

July 21, 2019 at 7:11pm
Hi, does anybody know how to make something like this work? For quickly prototyping it would be nice to have a typesafe set method.
import { Instance, types } from 'mobx-state-tree';
const storeFields = types.model({
text: types.string,
number: types.number,
bool: types.boolean
});
type IStoreFields = Instance<typeof storeFields>;
const store = storeFields.actions(self => ({
set(key: keyof IStoreFields, value: IStoreFields[typeof key]) {
self[key] = value; // TS-Error: Cannot assign to '[$stateTreeNodeType]' because it is a read-only property
}
}));
In JS it's no problem until it explodes at runtime because of an invalid combination of key and value.

July 21, 2019 at 9:27pm
You can use Object.assign(self, { [key]: value, }). However, Object.assign's return types are surprising: https://stackoverflow.com/questions/39906054/typescript-object-assign-confusion Personally I stopped using Object.assign for this very reason and I prefer strongly typed individual setters. Here's one way to do it:
set({ text = self.text, number = self.number, bool = self.bool, }) {
self.text = text
self.number = number
self.bool = bool
}
Yes, it is verbose and repetitive, which sucks. But at least it is type safe.
  • reply
  • like

November 29, 2019 at 10:45am
Hi , i was facing the same issue and i could solve your "read-only" error, simply by using type IStoreFields = SnapshotIn<typeof storeFields>; instead of "Instance"
like-fill
1
  • reply
  • like
interesting. Which typescript version do you use? And what are your settings in your tsconfig?
Because I tried to replicate this and got this error
  • reply
  • like
i double checked my test case and it seems to make a difference if the model has a single type of types (eg: types.string) or different ones, like in your sandbox.
i created a sandbox on my own and tried to figure it out and it seems to work now for multiple types. take a look and tell me if this may fix your problem:
https://codesandbox.io/s/pensive-mahavira-gh7lw?fontsize=14&hidenavigation=1&module=%2Fsrc%2Findex.ts&theme=dark
for the tsconfig i added the recommended flags from mst https://github.com/mobxjs/mobx-state-tree#recommend-compiler-flags
  • reply
  • like
Hi, thanks for replying
But this solutions looses the type safety. I added a test where one provides the wrong type and typescript can't catch it anymore.
  • reply
  • like
Impressive. I like it very much.
like-fill
1
  • reply
  • like

November 30, 2019 at 4:05am
This is awesome! I've been avoiding Object.assign because it isn't typesafe, but with this approach we can use it:
setUserDetails(userDetailsToSet: Partial<SnapshotIn<typeof self>>) {
Object.assign(self, userDetailsToSet)
},
We should PR it to the docs :)
  • reply
  • like

November 30, 2019 at 12:11pm
, do you want to write about it? Else I would write a blog post about it and of course give credit to you and link to this discussion.
  • reply
  • like

December 2, 2019 at 8:58am
feel free to write your blogpost. credit would be awesome ;)
  • reply
  • like

December 5, 2019 at 3:49pm
And there was another issue. Your solution didn't support optional types. I managed to use it with SnapshotIn but had to tell Typescript to ignore the line self[key] = value
Edited
  • reply
  • like

December 7, 2019 at 7:29am
what happens if you switch back from SnapshotIn to Instance?
  • reply
  • like

December 8, 2019 at 8:09am
The problem is in the types.optional- it accept a string or undefined but will be inferred as only string. Of SnapshotIn, SnpshotOut and Instance only SnapshotIn describes the types to set something correctly.
  • reply
  • like
This looks a little bit less brute force:
.actions(self => ({
set<
K extends keyof SnapshotIn<typeof self>,
T extends SnapshotIn<typeof self>
>(key: K, value: T[K]) {
self[key] = value as any;
}
}));
Edited
  • reply
  • like
ah i see ... i think any should never be a solution for something. seems like if we "cast" the value it allows types.optional to be undefined.
Edited
  • reply
  • like
Great, wasn't aware of cast :)
  • reply
  • like
Even if it as basically the same. We tell the compiler, it's okay.
  • reply
  • like

February 3, 2020 at 3:18pm
Hi, finally managed to write the article about it
like-fill
3
  • reply
  • like

November 5, 2020 at 8:10am
This is great! I was searching for this. Thank you very much and .
like-fill
1
  • reply
  • like