diff --git a/__tests__/draft.ts b/__tests__/draft.ts index 04c17b4a..6d17716d 100644 --- a/__tests__/draft.ts +++ b/__tests__/draft.ts @@ -1,5 +1,5 @@ import {assert, _} from "spec.ts" -import {Draft} from "../src/index" +import produce, {Draft, Draft, castDraft} from "../src/index" // For checking if a type is assignable to its draft type (and vice versa) const toDraft: (value: T) => Draft = x => x as any @@ -303,3 +303,18 @@ test("draft.ts", () => { expect(true).toBe(true) }) + +test("asDraft", () => { + type Todo = {readonly done: boolean} + + type State = { + readonly finishedTodos: ReadonlyArray + readonly unfinishedTodos: ReadonlyArray + } + + function markAllFinished(state: State) { + produce(state, draft => { + draft.finishedTodos = castDraft(state.unfinishedTodos) + }) + } +}) diff --git a/docs/api.md b/docs/api.md index c3ced846..802640b1 100644 --- a/docs/api.md +++ b/docs/api.md @@ -8,6 +8,8 @@ title: API overview | Exported name | Description | Section | | --- | --- | --- | | `applyPatches` | Given a base state or draft, and a set of patches, applies the patches | [Patches](patches.md) | +| `castDraft` | Converts any immutable type to its mutable counterpart. This is just a cast and doesn't actually do anything. | [TypeScript](typescript.md) | +| `castImmutable` | Converts any mutable type to its immutable counterpart. This is just a cast and doesn't actually do anything. | [TypeScript](typescript.md) | | `createDraft` | Given a base state, creates a mutable draft for which any modifications will be recorded | [Async](async.md) | | `Draft` | Exposed TypeScript type to convert an immutable type to a mutable type | [TypeScript](typescript.md) | | `finishDraft` | Given an draft created using `createDraft`, seals the draft and produces and returns the next immutable state that captures all the changes | [Async](async.md) | diff --git a/docs/typescript.md b/docs/typescript.md index 85fe393d..936467f1 100644 --- a/docs/typescript.md +++ b/docs/typescript.md @@ -66,6 +66,43 @@ const newState = increment(state, 2) _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)_ +## Cast utilities + +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: + +```typescript +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 assing 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 `asDraft`: + +`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. + +## Compatibility + +**Note:** Immer v5.3+ supports TypeScript v3.7+ only. + **Note:** Immer v1.9+ supports TypeScript v3.1+ only. **Note:** Immer v3.0+ supports TypeScript v3.4+ only. diff --git a/src/index.ts b/src/index.ts index 88c318ac..67b3fabf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,10 @@ -import {IProduce, IProduceWithPatches, Immer} from "./internal" +import { + IProduce, + IProduceWithPatches, + Immer, + Draft, + Immutable +} from "./internal" export { Draft, @@ -82,4 +88,23 @@ export const createDraft = immer.createDraft.bind(immer) */ export const finishDraft = immer.finishDraft.bind(immer) +/** + * This function is actually a no-op, but can be used to cast an immutable type + * to an draft type and make TypeScript happy + * + * @param value + */ +export function castDraft(value: T): Draft { + return value as any +} + +/** + * This function is actually a no-op, but can be used to cast a mutable type + * to an immutable type and make TypeScript happy + * @param value + */ +export function castImmutable(value: T): Immutable { + return value as any +} + export {Immer}