Skip to content

Commit

Permalink
feat: general typing improvements for TS 3.7 and up
Browse files Browse the repository at this point in the history
  • Loading branch information
mweststrate authored Jan 14, 2020
2 parents 9f4711a + 69cbf56 commit f930ce1
Show file tree
Hide file tree
Showing 16 changed files with 412 additions and 86 deletions.
14 changes: 14 additions & 0 deletions __tests__/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,20 @@ function runBaseTest(name, useProxies, autoFreeze, useListener) {
expect(d instanceof Map).toBeTruthy()
})
})

it("handles clear correctly", () => {
const map = new Map([
["a", 1],
["c", 3]
])
const next = produce(map, draft => {
draft.delete("a")
draft.set("b", 2)
draft.set("c", 4)
draft.clear()
})
expect(next).toEqual(new Map())
})
})

describe("set drafts", () => {
Expand Down
43 changes: 42 additions & 1 deletion __tests__/draft.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {assert, _} from "spec.ts"
import {Draft} from "../src/index"
import produce, {Draft, castDraft, original} 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 @@ -187,6 +187,13 @@ test("draft.ts", () => {
assert(fromDraft(toDraft(weak)), weak)
}

// ReadonlyMap instance
{
let val: ReadonlyMap<any, any> = _
let draft: Map<any, any> = _
assert(toDraft(val), draft)
}

// Set instance
{
let val: Set<any> = _
Expand All @@ -199,6 +206,13 @@ test("draft.ts", () => {
assert(fromDraft(toDraft(weak)), weak)
}

// ReadonlySet instance
{
let val: ReadonlySet<any> = _
let draft: Set<any> = _
assert(toDraft(val), draft)
}

// Promise object
{
let val: Promise<any> = _
Expand Down Expand Up @@ -289,3 +303,30 @@ 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)
})
}
})

test("#505 original", () => {
const baseState = {users: [{name: "Richie"}] as const}
const nextState = produce(baseState, draftState => {
original(draftState.users) === baseState.users
})
})

test("asDraft preserves a value", () => {
const x = {}
expect(castDraft(x)).toBe(x)
})
59 changes: 58 additions & 1 deletion __tests__/immutable.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {assert, _} from "spec.ts"
import {Immutable} from "../src/index"
import produce, {Immutable, castImmutable} from "../src/index"

test("types are ok", () => {
// array in tuple
Expand Down Expand Up @@ -56,5 +56,62 @@ test("types are ok", () => {
assert(val, _ as {readonly a: {readonly b: string}})
}

// Map
{
let val = _ as Immutable<Map<string, string>>
assert(val, _ as ReadonlyMap<string, string>)
}

// Already immutable Map
{
let val = _ as Immutable<ReadonlyMap<string, string>>
assert(val, _ as ReadonlyMap<string, string>)
}

// object in Map
{
let val = _ as Immutable<Map<{a: string}, {b: string}>>
assert(val, _ as ReadonlyMap<{readonly a: string}, {readonly b: string}>)
}

// Set
{
let val = _ as Immutable<Set<string>>
assert(val, _ as ReadonlySet<string>)
}

// Already immutable Set
{
let val = _ as Immutable<ReadonlySet<string>>
assert(val, _ as ReadonlySet<string>)
}

// object in Set
{
let val = _ as Immutable<Set<{a: string}>>
assert(val, _ as ReadonlySet<{readonly a: string}>)
}

expect(true).toBe(true)
})

test("#381 produce immutable state", () => {
const someState = {
todos: [
{
done: false
}
]
}

const immutable = castImmutable(produce(someState, _draft => {}))
assert(
immutable,
_ as {readonly todos: ReadonlyArray<{readonly done: boolean}>}
)
})

test("castImmutable preserves a value", () => {
const x = {}
expect(castImmutable(x)).toBe(x)
})
20 changes: 18 additions & 2 deletions __tests__/produce.ts
Original file line number Diff line number Diff line change
Expand Up @@ -438,12 +438,28 @@ it("works with readonly Map and Set", () => {
const s = new Set<S>([{x: 2}])

const res1 = produce(m, (draft: Draft<Map<string, S>>) => {
assert(draft, _ as Map<string, {readonly x: number}>) // TODO: drop readonly in TS 3.7
assert(draft, _ as Map<string, {x: number}>)
})
assert(res1, _ as Map<string, {readonly x: number}>)

const res2 = produce(s, (draft: Draft<Set<S>>) => {
assert(draft, _ as Set<{readonly x: number}>) // TODO: drop readonly in TS 3.7
assert(draft, _ as Set<{x: number}>)
})
assert(res2, _ as Set<{readonly x: number}>)
})

it("works with ReadonlyMap and ReadonlySet", () => {
type S = {readonly x: number}
const m: ReadonlyMap<string, S> = new Map([["a", {x: 1}]])
const s: ReadonlySet<S> = new Set([{x: 2}])

const res1 = produce(m, (draft: Draft<Map<string, S>>) => {
assert(draft, _ as Map<string, {x: number}>)
})
assert(res1, _ as ReadonlyMap<string, {readonly x: number}>)

const res2 = produce(s, (draft: Draft<Set<S>>) => {
assert(draft, _ as Set<{x: number}>)
})
assert(res2, _ as ReadonlySet<{readonly x: number}>)
})
101 changes: 101 additions & 0 deletions __tests__/redux.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import {assert, _} from "spec.ts"
import produce, {
produce as produce2,
applyPatches,
Patch,
nothing,
Draft,
Immutable
} from "../src/"
import * as redux from "redux"

export interface State {
counter: number
}

export interface Action {
type: string
payload: number
}

export const initialState: State = {
counter: 0
}

/// =============== Actions

export function addToCounter(addNumber: number) {
return {
type: "ADD_TO_COUNTER",
payload: addNumber
}
}

export function subFromCounter(subNumber: number) {
return {
type: "SUB_FROM_COUNTER",
payload: subNumber
}
}

export const reduceCounterProducer = (
state: State = initialState,
action: Action
) =>
produce(state, draftState => {
switch (action.type) {
case "ADD_TO_COUNTER":
draftState.counter += action.payload
break
case "SUB_FROM_COUNTER":
draftState.counter -= action.payload
break
}
})

export const reduceCounterCurriedProducer = produce(
(draftState: Draft<State>, action: Action) => {
switch (action.type) {
case "ADD_TO_COUNTER":
draftState.counter += action.payload
break
case "SUB_FROM_COUNTER":
draftState.counter -= action.payload
break
}
},
initialState
)

/// =============== Reducers

export const reduce = redux.combineReducers({
counterReducer: reduceCounterProducer
})

export const curredReduce = redux.combineReducers({
counterReducer: reduceCounterCurriedProducer
})

// reducing the current state to get the next state!
// console.log(reduce(initialState, addToCounter(12));

// ================ store

export const store = redux.createStore(reduce)
export const curriedStore = redux.createStore(curredReduce)

it("#470 works with Redux combine reducers", () => {
assert(
store.getState().counterReducer,
_ as {
counter: number
}
)
assert(
curriedStore.getState().counterReducer,
_ as {
readonly counter: number
}
)
})
2 changes: 2 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
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
57 changes: 57 additions & 0 deletions docs/typescript.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,63 @@ 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.

Tip: You can combine `castImmutable` with `produce` to type the return type of `produce` as something immutable, even when the original state was mutable:

```typescript
// 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
}>
})
```

## 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
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"test:perf": "NODE_ENV=production yarn-or-npm build && cd __performance_tests__ && babel-node add-data.js && babel-node todo.js && babel-node incremental.js",
"test:flow": "yarn-or-npm flow check __tests__/flow",
"watch": "jest --watch",
"coverage": "jest --coverage",
"coveralls": "jest --coverage && cat ./coverage/lcov.info | ./node_modules/.bin/coveralls && rm -rf ./coverage",
"build": "rimraf dist/ && yarn-or-npm bili && yarn-or-npm typed",
"typed": "cpx 'src/immer.js.flow' dist -v",
Expand Down Expand Up @@ -72,6 +73,7 @@
"lodash.clonedeep": "^4.5.0",
"prettier": "1.19.1",
"pretty-quick": "^1.8.0",
"redux": "^4.0.5",
"regenerator-runtime": "^0.11.1",
"rimraf": "^2.6.2",
"rollup-plugin-typescript2": "^0.25.3",
Expand Down
3 changes: 2 additions & 1 deletion src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ export function isPlainObject(value: any): boolean {
}

/** Get the underlying object that is represented by the given draft */
export function original<T>(value: Drafted<T>): T | undefined {
export function original<T>(value: T): T | undefined
export function original(value: Drafted<any>): any {
if (value && value[DRAFT_STATE]) {
return value[DRAFT_STATE].base as any
}
Expand Down
1 change: 1 addition & 0 deletions src/extends.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* istanbul ignore next */
var extendStatics = function(d: any, b: any): any {
extendStatics =
Object.setPrototypeOf ||
Expand Down
Loading

0 comments on commit f930ce1

Please sign in to comment.