id | title | sidebar_label |
---|---|---|
typescript |
Using TypeScript or Flow |
TypeScript / Flow |
egghead.io lesson 12: Immer + TypeScript
The Immer package ships with type definitions inside the package, which should be picked up by TypeScript and Flow out of the box and without further configuration.
The TypeScript typings automatically remove readonly
modifiers from your draft types and return a value that matches your original type. See this practical example:
import produce from "immer"
interface State {
readonly x: number
}
// `x` cannot be modified here
const state: State = {
x: 0
}
const newState = produce(state, draft => {
// `x` can be modified here
draft.x++
})
// `newState.x` cannot be modified here
This ensures that the only place you can modify your state is in your produce callbacks. It even works recursively and with ReadonlyArray
!
For curried reducers, the type is inferred from the first argument of recipe function, so make sure to type it. The Draft
utility type can be used if the state argument type is immutable:
import produce, {Draft} from "immer"
interface State {
readonly x: number
}
// `x` cannot be modified here
const state: State = {
x: 0
}
const increment = produce((draft: Draft<State>, inc: number) => {
// `x` can be modified here
draft.x += inc
})
const newState = increment(state, 2)
// `newState.x` cannot be modified here
Note: Since TypeScript support for recursive types is limited, and there is no co- contravariance, it might the easiest to not type your state as readonly
(Immer will still protect against accidental mutations)
The types inside and outside a produce
can be conceptually the same, but from a practical perspective different. For example, the State
in the examples above should be considered immutable outside produce
, but mutable inside produce
.
Sometimes this leads to practical conflicts. Take the following example:
type Todo = {readonly done: boolean}
type State = {
readonly finishedTodos: readonly Todo[]
readonly unfinishedTodos: readonly Todo[]
}
function markAllFinished(state: State) {
produce(state, draft => {
draft.finishedTodos = state.unfinishedTodos
})
}
This will generate the error:
The type 'readonly Todo[]' is 'readonly' and cannot be assigned to the mutable type '{ done: boolean; }[]'
The reason for this error is that we assign our read only, immutable array to our draft, which expects a mutable type, with methods like .push
etc etc. As far as TS is concerned, those are not exposed from our original State
. To hint TypeScript that we want to upcast the collection here to a mutable array for draft purposes, we can use the utility castDraft
:
draft.finishedTodos = castDraft(state.unfinishedTodos)
will make the error disappear.
There is also the utility castImmutable
, in case you ever need to achieve the opposite. Note that these utilities are for all practical purposes no-ops, they will just return their original value.
Tip: You can combine castImmutable
with produce
to type the return type of produce
as something immutable, even when the original state was mutable:
// a mutable data structure
const baseState = {
todos: [{
done: false
}]
}
const nextState = castImmutable(produce(baseState, _draft => {}))
// inferred type of nextState is now:
{
readonly todos: ReadonlyArray<{
readonly done: boolean
}>
})
Note: Immer v5.3+ supports TypeScript v3.7+ only.
Note: Immer v3.0+ supports TypeScript v3.4+ only.
Note: Immer v1.9+ supports TypeScript v3.1+ only.
Note: Flow support might be removed in future versions and we recommend TypeScript