-
-
Notifications
You must be signed in to change notification settings - Fork 842
/
produce.ts
286 lines (249 loc) · 7.94 KB
/
produce.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
import produce, {
produce as produce2,
applyPatches,
Patch,
nothing,
Draft
} from "../dist/immer.js"
// prettier-ignore
type Exact<A, B> = (<T>() => T extends A ? 1 : 0) extends (<T>() => T extends B ? 1 : 0)
? (A extends B ? (B extends A ? unknown : never) : never)
: never
/** Fails when `actual` and `expected` have different types. */
declare const exactType: <Actual, Expected>(
actual: Actual & Exact<Actual, Expected>,
expected: Expected & Exact<Actual, Expected>
) => Expected
interface State {
readonly num: number
readonly foo?: string
bar: string
readonly baz: {
readonly x: number
readonly y: number
}
readonly arr: ReadonlyArray<{readonly value: string}>
readonly arr2: {readonly value: string}[]
}
const state: State = {
num: 0,
bar: "foo",
baz: {
x: 1,
y: 2
},
arr: [{value: "asdf"}],
arr2: [{value: "asdf"}]
}
const expectedState: State = {
num: 1,
foo: "bar",
bar: "foo",
baz: {
x: 2,
y: 3
},
arr: [{value: "foo"}, {value: "asf"}],
arr2: [{value: "foo"}, {value: "asf"}]
}
it("can update readonly state via standard api", () => {
const newState = produce<State>(state, draft => {
draft.num++
draft.foo = "bar"
draft.bar = "foo"
draft.baz.x++
draft.baz.y++
draft.arr[0].value = "foo"
draft.arr.push({value: "asf"})
draft.arr2[0].value = "foo"
draft.arr2.push({value: "asf"})
})
exactType(newState, state)
})
// NOTE: only when the function type is inferred
it("can infer state type from default state", () => {
type State = {readonly a: number} | boolean
type Recipe = (base?: State | undefined) => State
let foo = produce(_ => {}, {} as State)
exactType(foo, {} as Recipe)
})
it("can infer state type from recipe function", () => {
type State = {readonly a: string} | {readonly b: string}
type Recipe = (base: State | undefined, arg: number) => State
let foo = produce((draft: Draft<State>, arg: number) => {}, {} as any)
exactType(foo, {} as Recipe)
})
it("cannot infer state type when the function type and default state are missing", () => {
const res = produce(_ => {})
exactType(res, {} as (base: any) => any)
})
it("can update readonly state via curried api", () => {
const newState = produce<State>(draft => {
draft.num++
draft.foo = "bar"
draft.bar = "foo"
draft.baz.x++
draft.baz.y++
draft.arr[0].value = "foo"
draft.arr.push({value: "asf"})
draft.arr2[0].value = "foo"
draft.arr2.push({value: "asf"})
})(state)
expect(newState).not.toBe(state)
expect(newState).toEqual(expectedState)
})
it("can update use the non-default export", () => {
const newState = produce2<State>(draft => {
draft.num++
draft.foo = "bar"
draft.bar = "foo"
draft.baz.x++
draft.baz.y++
draft.arr[0].value = "foo"
draft.arr.push({value: "asf"})
draft.arr2[0].value = "foo"
draft.arr2.push({value: "asf"})
})(state)
expect(newState).not.toBe(state)
expect(newState).toEqual(expectedState)
})
it("can apply patches", () => {
let patches: Patch[] = []
produce(
{x: 3},
d => {
d.x++
},
p => {
patches = p
}
)
expect(applyPatches({}, patches)).toEqual({x: 4})
})
describe("curried producer", () => {
it("supports rest parameters", () => {
type State = {readonly a: 1}
// No initial state:
let foo = produce<State, number[]>(() => {})
exactType(foo, {} as (base: State, ...args: number[]) => State)
foo({} as State, 1, 2)
// TODO: Using argument parameters
// let woo = produce((state: Draft<State>, ...args: number[]) => {})
// exactType(woo, {} as (base: State, ...args: number[]) => State)
// woo({} as State, 1, 2)
// With initial state:
let bar = produce(
(state: Draft<State>, ...args: number[]) => {},
{} as State
)
exactType(bar, {} as (base?: State, ...args: number[]) => State)
bar({} as State | undefined, 1, 2)
bar()
// When args is a tuple:
let tup = produce(
(state: Draft<State>, ...args: [string, ...number[]]) => {},
{} as State
)
exactType(tup, {} as (
base: State | undefined,
arg1: string,
...args: number[]
) => State)
tup({a: 1}, "", 2)
tup(undefined, "", 2)
})
it("can be passed a readonly array", () => {
// No initial state:
let foo = produce<ReadonlyArray<any>>(() => {})
exactType(foo, {} as (base: readonly any[]) => readonly any[])
foo([] as ReadonlyArray<any>)
// With initial state:
let bar = produce(() => {}, [] as ReadonlyArray<any>)
exactType(bar, {} as (base?: readonly any[]) => readonly any[])
bar([] as ReadonlyArray<any>)
bar(undefined)
bar()
})
})
it("works with return type of: number", () => {
let base = {} as {a: number}
let result = produce(base, () => 1)
exactType(result, {} as number)
})
it("works with return type of: number | undefined", () => {
let base = {} as {a: number}
let result = produce(base, draft => {
return draft.a < 0 ? 0 : undefined
})
exactType(result, {} as {a: number} | number)
})
it("can return an object type that is identical to the base type", () => {
let base = {} as {a: number}
let result = produce(base, draft => {
return draft.a < 0 ? {a: 0} : undefined
})
// TODO: Can we resolve the weird union of identical object types?
exactType(result, {} as {a: number} | {a: number})
})
it("can return an object type that is _not_ assignable to the base type", () => {
let base = {} as {a: number}
let result = produce(base, draft => {
return draft.a < 0 ? {a: true} : undefined
})
exactType(result, {} as {a: number} | {a: boolean})
})
it("does not enforce immutability at the type level", () => {
let result = produce([] as any[], draft => {
draft.push(1)
})
exactType(result, {} as any[])
})
it("can produce an undefined value", () => {
let base = {} as {readonly a: number}
// Return only nothing.
let result = produce(base, _ => nothing)
exactType(result, undefined)
// Return maybe nothing.
let result2 = produce(base, draft => {
if (draft.a > 0) return nothing
})
exactType(result2, {} as typeof base | undefined)
})
it("can return the draft itself", () => {
let base = {} as {readonly a: number}
let result = produce(base, draft => draft)
// Currently, the `readonly` modifier is lost.
exactType(result, {} as {a: number} | undefined)
})
it("can return a promise", () => {
type Base = {readonly a: number}
let base = {} as Base
// Return a promise only.
let res1 = produce(base, draft => {
return Promise.resolve(draft.a > 0 ? null : undefined)
})
exactType(res1, {} as Promise<Base | null>)
// Return a promise or undefined.
let res2 = produce(base, draft => {
if (draft.a > 0) return Promise.resolve()
})
exactType(res2, {} as Base | Promise<Base>)
})
it("works with `void` hack", () => {
let base = {} as {readonly a: number}
let copy = produce(base, s => void s.a++)
exactType(copy, base)
})
it("works with generic parameters", () => {
let insert = <T>(array: ReadonlyArray<T>, index: number, elem: T) => {
// NOTE: As of 3.2.2, the explicit argument type is required.
return produce(array, (draft: T[]) => {
draft.push(elem)
draft.splice(index, 0, elem)
draft.concat([elem])
})
}
let val: {readonly a: ReadonlyArray<number>} = 0 as any
let arr: ReadonlyArray<typeof val> = 0 as any
insert(arr, 0, val)
})