-
-
Notifications
You must be signed in to change notification settings - Fork 866
Improves type safety in immer.d.ts and fixes 97 #109
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
- Fixed two-arg produce() curries being returned as one-arg curries - Added a one-arg produce() curry signature - Current state may now be Readonly<T> via new type MaybeReadonly<T> - produce() and curries now return Readonly<T> - Added Draft<T> that ensures the draft is always writable (fixes 97) - Reversed the order of produce() signatures so that the most specific is used first - Added non-doc style comments to curry-producing produce() variants - Added a note to the varadic signature of produce() about Typescript issue 5453
|
The following case fails: interface State extends Readonly<{
foo: {
readonly val: boolean;
}
}> {}
const state: State = {
foo: {
val: true
}
};
produce(state, (draft: Draft<State>) => {
draft.foo.val = false; // error TS2540: Cannot assign to 'val' because it is a constant or a read-only property.
});Also, my IDE completely fails to give me suggestions for |
|
Yes but this is only because you have a readonly within a readonly. That's very unlikely to happen. |
|
You do need to explicitly specify the type of draft with this change, either as a type param to the produce invocation or by declaring the type of the argument(s) of the function you pass to it. Otherwise it will default to Edit: "of the function" -> "of the argument(s) of the function" |
Likeliness has nothing to do with it. Someone will have a reason to do it, and this change won't support it. Plus it's easy to come up with a use case in Redux: export interface AuthState extends Readonly<{
user: {
email: string;
name: string;
}
}> {}
export interface RouterState extends Readonly<{
pathname: string;
}> {}
interface RootState extends Readonly<{
auth: AuthState;
router: RouterState;
}> {}Now, there might be a reducer operating on This was also the reason (one reason anyway) why my PR wasn't accepted. |
|
You're right. Pick (and therefore Draft, as it's essentially the same) preserve the readonliness of properties. This will never be possible. Typescript 2.8 is (I believe) introducing the ability to add/remove readonly/optional "flags" of a property however that also breaks interfaces where there are properties that must STAY readonly. Maybe there's another way but I'm spent thinking about this for now. |
|
My proposal is recipe: (this: S, draftState: S | any) => voidAlthough it doesn't strongly enforce types, it removes the read-onlyness while IDEs can still give suggestions. Until we can recursively remove read-only flags from interfaces, I think this isn't so bad. |
|
@tilastokeskus isn't it simpler to define your types in two stages? |
|
|
||
| // 0 additional arguments | ||
| export default function<S = any>( | ||
| recipe: (this: Readonly<S>, draftState: Draft<S>) => void |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this be Draft<S> as well? (it's the same thing)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah no because that would allow modification of the original state. With it being Readonly<S> the compiler complains if this is modified in the recipe.
interface State { foo: string }
const first = { foo: "bar" };
const second = produce(state, draft => {
this.foo = "can't do this, compiler error because 'this.foo' is readonly";
draft.foo = "this works fine though";
});
second.foo = "this also produces a compiler error";
const third = produce(second, draft => {
draft.foo = "but this does work, woo!";
});The only time I can see this patch posing a problem is if someone defines their interfaces as completely readonly from the start. TS 2.8 is adding the ability to add/remove the readonly modifier when creating a new interface that references the type of a property of another interface (as in Draft/Pick/Readonly etc) but even then it wouldn't be possible to obtain the original readonly status.
As you said in your comment above the sensible thing to do is to define the interfaces in two steps. That also allows some properties to be readonly permanently such as:
interface State {
readonly appName: string;
someValue: number;
}
const first: State = { appName: "My App", someValue: 0 };
const second = produce(first, draft => {
draft.appName = "compiler error here";
draft.someValue++; // fine
});If there's a chance there are people already using immer who have made everything readonly from the start this would be a breaking change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry @mweststrate when you said "they're the same" - they are literally the same object! My understanding was that this is the original state. So you could do:
const original = { foo: 123, diff: 0 };
const next = produce(original, draft => {
draft.foo += someValue;
draft.diff = this.foo - draft.foo;
});But that's not the case. Though perhaps it should be?
Edit: Because you might not always have a reference to the original in a producer function created via produce(func) but might need it. This would be another breaking change but you don't need me to say that!
I would rather not make special types if immer is the only reason to do so. I'd much rather just say produce(original, (draft: AuthState | any) => { ... });and wait for typescript 2.8 in the hopes of being able to remove readonly properties recursively. |
|
@ogwh I cherrypicked all the changes except the ones around readonly state. Would you mind checking whether that could be implemented accurately and recursively in TS 2.8 and updating this PR? Thanks! |
|
Solving the readonly typings would really be great. In one blow that would make this library the de-facto immutability library for typescript, as you will have a simple and concise, yet typesafe way of mutating your immutable objects. |
Summary
specific is used first
issue 5453
Examples