Skip to content

Commit

Permalink
Introduced castDraft / castImmutable, implements #486
Browse files Browse the repository at this point in the history
  • Loading branch information
mweststrate committed Jan 14, 2020
1 parent 4204af5 commit 2d27f33
Show file tree
Hide file tree
Showing 4 changed files with 81 additions and 2 deletions.
17 changes: 16 additions & 1 deletion __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: <T>(value: T) => Draft<T> = x => x as any
Expand Down Expand Up @@ -303,3 +303,18 @@ test("draft.ts", () => {

expect(true).toBe(true)
})

test("asDraft", () => {
type Todo = {readonly done: boolean}

type State = {
readonly finishedTodos: ReadonlyArray<Todo>
readonly unfinishedTodos: ReadonlyArray<Todo>
}

function markAllFinished(state: State) {
produce(state, draft => {
draft.finishedTodos = castDraft(state.unfinishedTodos)
})
}
})
2 changes: 2 additions & 0 deletions docs/api.md
Expand Up @@ -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<T>` | 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) |
Expand Down
37 changes: 37 additions & 0 deletions docs/typescript.md
Expand Up @@ -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.
Expand Down
27 changes: 26 additions & 1 deletion src/index.ts
@@ -1,4 +1,10 @@
import {IProduce, IProduceWithPatches, Immer} from "./internal"
import {
IProduce,
IProduceWithPatches,
Immer,
Draft,
Immutable
} from "./internal"

export {
Draft,
Expand Down Expand Up @@ -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<T>(value: T): Draft<T> {
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<T>(value: T): Immutable<T> {
return value as any
}

export {Immer}

0 comments on commit 2d27f33

Please sign in to comment.