1 | import type { Action, AnyAction, Reducer } from 'redux'
|
2 | import type {
|
3 | ActionCreatorWithNonInferrablePayload,
|
4 | ActionCreatorWithOptionalPayload,
|
5 | ActionCreatorWithoutPayload,
|
6 | ActionCreatorWithPayload,
|
7 | ActionCreatorWithPreparedPayload,
|
8 | ActionReducerMapBuilder,
|
9 | PayloadAction,
|
10 | SliceCaseReducers,
|
11 | ValidateSliceCaseReducers,
|
12 | } from '@reduxjs/toolkit'
|
13 | import { createAction, createSlice } from '@reduxjs/toolkit'
|
14 | import { expectType } from './helpers'
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 | const counterSlice = createSlice({
|
21 | name: 'counter',
|
22 | initialState: 0,
|
23 | reducers: {
|
24 | increment: (state: number, action) => state + action.payload,
|
25 | decrement: (state: number, action) => state - action.payload,
|
26 | },
|
27 | })
|
28 |
|
29 | const uiSlice = createSlice({
|
30 | name: 'ui',
|
31 | initialState: 0,
|
32 | reducers: {
|
33 | goToNext: (state: number, action) => state + action.payload,
|
34 | goToPrevious: (state: number, action) => state - action.payload,
|
35 | },
|
36 | })
|
37 |
|
38 | const actionCreators = {
|
39 | [counterSlice.name]: { ...counterSlice.actions },
|
40 | [uiSlice.name]: { ...uiSlice.actions },
|
41 | }
|
42 |
|
43 | expectType<typeof counterSlice.actions>(actionCreators.counter)
|
44 | expectType<typeof uiSlice.actions>(actionCreators.ui)
|
45 |
|
46 |
|
47 | const value = actionCreators.anyKey
|
48 |
|
49 |
|
50 |
|
51 |
|
52 | {
|
53 | const firstAction = createAction<{ count: number }>('FIRST_ACTION')
|
54 |
|
55 | const slice = createSlice({
|
56 | name: 'counter',
|
57 | initialState: 0,
|
58 | reducers: {
|
59 | increment: (state: number, action) => state + action.payload,
|
60 | decrement: (state: number, action) => state - action.payload,
|
61 | },
|
62 | extraReducers: {
|
63 | [firstAction.type]: (state: number, action) =>
|
64 | state + action.payload.count,
|
65 | },
|
66 | })
|
67 |
|
68 |
|
69 |
|
70 | const reducer: Reducer<number, PayloadAction> = slice.reducer
|
71 |
|
72 |
|
73 | const stringReducer: Reducer<string, PayloadAction> = slice.reducer
|
74 |
|
75 | const anyActionReducer: Reducer<string, AnyAction> = slice.reducer
|
76 |
|
77 |
|
78 |
|
79 | slice.actions.increment(1)
|
80 | slice.actions.decrement(1)
|
81 |
|
82 |
|
83 | slice.actions.other(1)
|
84 | }
|
85 |
|
86 |
|
87 |
|
88 |
|
89 | {
|
90 | const counter = createSlice({
|
91 | name: 'counter',
|
92 | initialState: 0,
|
93 | reducers: {
|
94 | increment: (state) => state + 1,
|
95 | decrement: (state, { payload = 1 }: PayloadAction<number | undefined>) =>
|
96 | state - payload,
|
97 | multiply: (state, { payload }: PayloadAction<number | number[]>) =>
|
98 | Array.isArray(payload)
|
99 | ? payload.reduce((acc, val) => acc * val, state)
|
100 | : state * payload,
|
101 | addTwo: {
|
102 | reducer: (s, { payload }: PayloadAction<number>) => s + payload,
|
103 | prepare: (a: number, b: number) => ({
|
104 | payload: a + b,
|
105 | }),
|
106 | },
|
107 | },
|
108 | })
|
109 |
|
110 | expectType<ActionCreatorWithoutPayload>(counter.actions.increment)
|
111 | counter.actions.increment()
|
112 |
|
113 | expectType<ActionCreatorWithOptionalPayload<number | undefined>>(
|
114 | counter.actions.decrement
|
115 | )
|
116 | counter.actions.decrement()
|
117 | counter.actions.decrement(2)
|
118 |
|
119 | expectType<ActionCreatorWithPayload<number | number[]>>(
|
120 | counter.actions.multiply
|
121 | )
|
122 | counter.actions.multiply(2)
|
123 | counter.actions.multiply([2, 3, 4])
|
124 |
|
125 | expectType<ActionCreatorWithPreparedPayload<[number, number], number>>(
|
126 | counter.actions.addTwo
|
127 | )
|
128 | counter.actions.addTwo(1, 2)
|
129 |
|
130 |
|
131 | counter.actions.multiply()
|
132 |
|
133 |
|
134 | counter.actions.multiply('2')
|
135 |
|
136 |
|
137 | counter.actions.addTwo(1)
|
138 | }
|
139 |
|
140 |
|
141 |
|
142 |
|
143 | {
|
144 | const counter = createSlice({
|
145 | name: 'counter',
|
146 | initialState: 0,
|
147 | reducers: {
|
148 | increment: (state) => state + 1,
|
149 | decrement: (state) => state - 1,
|
150 | multiply: (state, { payload }: PayloadAction<number | number[]>) =>
|
151 | Array.isArray(payload)
|
152 | ? payload.reduce((acc, val) => acc * val, state)
|
153 | : state * payload,
|
154 | },
|
155 | })
|
156 |
|
157 | const s: string = counter.actions.increment.type
|
158 | const t: string = counter.actions.decrement.type
|
159 | const u: string = counter.actions.multiply.type
|
160 |
|
161 |
|
162 | const x: 'counter/increment' = counter.actions.increment.type
|
163 |
|
164 | const y: 'increment' = counter.actions.increment.type
|
165 | }
|
166 |
|
167 |
|
168 |
|
169 |
|
170 | {
|
171 | const counter = createSlice({
|
172 | name: 'test',
|
173 | initialState: { counter: 0, concat: '' },
|
174 | reducers: {
|
175 | incrementByStrLen: {
|
176 | reducer: (state, action: PayloadAction<number>) => {
|
177 | state.counter += action.payload
|
178 | },
|
179 | prepare: (payload: string) => ({
|
180 | payload: payload.length,
|
181 | }),
|
182 | },
|
183 | concatMetaStrLen: {
|
184 | reducer: (state, action: PayloadAction<string>) => {
|
185 | state.concat += action.payload
|
186 | },
|
187 | prepare: (payload: string) => ({
|
188 | payload,
|
189 | meta: payload.length,
|
190 | }),
|
191 | },
|
192 | },
|
193 | })
|
194 |
|
195 | expectType<string>(counter.actions.incrementByStrLen('test').type)
|
196 | expectType<number>(counter.actions.incrementByStrLen('test').payload)
|
197 | expectType<string>(counter.actions.concatMetaStrLen('test').payload)
|
198 | expectType<number>(counter.actions.concatMetaStrLen('test').meta)
|
199 |
|
200 |
|
201 | expectType<string>(counter.actions.incrementByStrLen('test').payload)
|
202 |
|
203 |
|
204 | expectType<string>(counter.actions.concatMetaStrLen('test').meta)
|
205 | }
|
206 |
|
207 |
|
208 |
|
209 |
|
210 | {
|
211 | const counter = createSlice({
|
212 | name: 'test',
|
213 | initialState: { counter: 0, concat: '' },
|
214 | reducers: {
|
215 |
|
216 | testDefaultMetaAndError: {
|
217 | reducer(_, action: PayloadAction<number, string>) {},
|
218 | prepare: (payload: number) => ({
|
219 | payload,
|
220 | meta: 'meta' as 'meta',
|
221 | error: 'error' as 'error',
|
222 | }),
|
223 | },
|
224 |
|
225 | testUnknownMetaAndError: {
|
226 | reducer(_, action: PayloadAction<number, string, unknown, unknown>) {},
|
227 | prepare: (payload: number) => ({
|
228 | payload,
|
229 | meta: 'meta' as 'meta',
|
230 | error: 'error' as 'error',
|
231 | }),
|
232 | },
|
233 |
|
234 | testMetaAndError: {
|
235 | reducer(_, action: PayloadAction<number, string, 'meta', 'error'>) {},
|
236 | prepare: (payload: number) => ({
|
237 | payload,
|
238 | meta: 'meta' as 'meta',
|
239 | error: 'error' as 'error',
|
240 | }),
|
241 | },
|
242 |
|
243 | testErroneousMeta: {
|
244 | reducer(_, action: PayloadAction<number, string, 'meta', 'error'>) {},
|
245 |
|
246 | prepare: (payload: number) => ({
|
247 | payload,
|
248 | meta: 1,
|
249 | error: 'error' as 'error',
|
250 | }),
|
251 | },
|
252 |
|
253 | testErroneousError: {
|
254 | reducer(_, action: PayloadAction<number, string, 'meta', 'error'>) {},
|
255 |
|
256 | prepare: (payload: number) => ({
|
257 | payload,
|
258 | meta: 'meta' as 'meta',
|
259 | error: 1,
|
260 | }),
|
261 | },
|
262 | },
|
263 | })
|
264 | }
|
265 |
|
266 |
|
267 |
|
268 |
|
269 | {
|
270 | const counter = createSlice({
|
271 | name: 'counter',
|
272 | initialState: 0,
|
273 | reducers: {
|
274 | increment(state, action: PayloadAction<number>) {
|
275 | return state + action.payload
|
276 | },
|
277 | decrement: {
|
278 | reducer(state, action: PayloadAction<number>) {
|
279 | return state - action.payload
|
280 | },
|
281 | prepare(amount: number) {
|
282 | return { payload: amount }
|
283 | },
|
284 | },
|
285 | },
|
286 | })
|
287 |
|
288 |
|
289 | expectType<(state: number, action: PayloadAction<number>) => number | void>(
|
290 | counter.caseReducers.increment
|
291 | )
|
292 |
|
293 |
|
294 | expectType<(state: number, action: PayloadAction<number>) => number | void>(
|
295 | counter.caseReducers.decrement
|
296 | )
|
297 |
|
298 |
|
299 |
|
300 | expectType<(state: number, action: PayloadAction<string>) => number | void>(
|
301 |
|
302 | counter.caseReducers.increment
|
303 | )
|
304 |
|
305 |
|
306 |
|
307 | expectType<(state: number, action: PayloadAction<string>) => number | void>(
|
308 |
|
309 | counter.caseReducers.decrement
|
310 | )
|
311 |
|
312 |
|
313 |
|
314 | expectType<(state: number, action: PayloadAction<string>) => number | void>(
|
315 |
|
316 | counter.caseReducers.someThingNonExistant
|
317 | )
|
318 | }
|
319 |
|
320 |
|
321 |
|
322 |
|
323 | {
|
324 | const counter = createSlice({
|
325 | name: 'counter',
|
326 | initialState: { counter: 0 },
|
327 | reducers: {
|
328 | increment: {
|
329 | reducer(state, action: PayloadAction<string>) {
|
330 | state.counter += action.payload.length
|
331 | },
|
332 |
|
333 | prepare(x: string) {
|
334 | return {
|
335 | payload: 6,
|
336 | }
|
337 | },
|
338 | },
|
339 | },
|
340 | })
|
341 | }
|
342 |
|
343 |
|
344 |
|
345 |
|
346 |
|
347 | {
|
348 | const initialState = {
|
349 | name: null,
|
350 | }
|
351 |
|
352 | const mySlice = createSlice({
|
353 | name: 'name',
|
354 | initialState,
|
355 | reducers: {
|
356 | setName: (state, action) => {
|
357 | state.name = action.payload
|
358 | },
|
359 | },
|
360 | })
|
361 |
|
362 | expectType<ActionCreatorWithNonInferrablePayload>(mySlice.actions.setName)
|
363 |
|
364 | const x = mySlice.actions.setName
|
365 |
|
366 | mySlice.actions.setName(null)
|
367 | mySlice.actions.setName('asd')
|
368 | mySlice.actions.setName(5)
|
369 | }
|
370 |
|
371 |
|
372 |
|
373 |
|
374 | {
|
375 | const mySlice = createSlice({
|
376 | name: 'name',
|
377 | initialState: { name: 'test' },
|
378 | reducers: {
|
379 | setName: (state, action: PayloadAction<string>) => {
|
380 | state.name = action.payload
|
381 | },
|
382 | },
|
383 | })
|
384 |
|
385 | const x: Action<unknown> = {} as any
|
386 | if (mySlice.actions.setName.match(x)) {
|
387 | expectType<string>(x.type)
|
388 | expectType<string>(x.payload)
|
389 | } else {
|
390 |
|
391 | expectType<string>(x.type)
|
392 |
|
393 | expectType<string>(x.payload)
|
394 | }
|
395 | }
|
396 |
|
397 |
|
398 | {
|
399 | createSlice({
|
400 | name: 'test',
|
401 | initialState: 0,
|
402 | reducers: {},
|
403 | extraReducers: (builder) => {
|
404 | expectType<ActionReducerMapBuilder<number>>(builder)
|
405 | },
|
406 | })
|
407 | }
|
408 |
|
409 |
|
410 | {
|
411 | interface GenericState<T> {
|
412 | data?: T
|
413 | status: 'loading' | 'finished' | 'error'
|
414 | }
|
415 |
|
416 | const createGenericSlice = <
|
417 | T,
|
418 | Reducers extends SliceCaseReducers<GenericState<T>>
|
419 | >({
|
420 | name = '',
|
421 | initialState,
|
422 | reducers,
|
423 | }: {
|
424 | name: string
|
425 | initialState: GenericState<T>
|
426 | reducers: ValidateSliceCaseReducers<GenericState<T>, Reducers>
|
427 | }) => {
|
428 | return createSlice({
|
429 | name,
|
430 | initialState,
|
431 | reducers: {
|
432 | start(state) {
|
433 | state.status = 'loading'
|
434 | },
|
435 | success(state: GenericState<T>, action: PayloadAction<T>) {
|
436 | state.data = action.payload
|
437 | state.status = 'finished'
|
438 | },
|
439 | ...reducers,
|
440 | },
|
441 | })
|
442 | }
|
443 |
|
444 | const wrappedSlice = createGenericSlice({
|
445 | name: 'test',
|
446 | initialState: { status: 'loading' } as GenericState<string>,
|
447 | reducers: {
|
448 | magic(state) {
|
449 | expectType<GenericState<string>>(state)
|
450 |
|
451 | expectType<GenericState<number>>(state)
|
452 |
|
453 | state.status = 'finished'
|
454 | state.data = 'hocus pocus'
|
455 | },
|
456 | },
|
457 | })
|
458 |
|
459 | expectType<ActionCreatorWithPayload<string>>(wrappedSlice.actions.success)
|
460 | expectType<ActionCreatorWithoutPayload<string>>(wrappedSlice.actions.magic)
|
461 | }
|
462 |
|
463 | {
|
464 | interface GenericState<T> {
|
465 | data: T | null
|
466 | }
|
467 |
|
468 | function createDataSlice<
|
469 | T,
|
470 | Reducers extends SliceCaseReducers<GenericState<T>>
|
471 | >(
|
472 | name: string,
|
473 | reducers: ValidateSliceCaseReducers<GenericState<T>, Reducers>,
|
474 | initialState: GenericState<T>
|
475 | ) {
|
476 | const doNothing = createAction<undefined>('doNothing')
|
477 | const setData = createAction<T>('setData')
|
478 |
|
479 | const slice = createSlice({
|
480 | name,
|
481 | initialState,
|
482 | reducers,
|
483 | extraReducers: (builder) => {
|
484 | builder.addCase(doNothing, (state) => {
|
485 | return { ...state }
|
486 | })
|
487 | builder.addCase(setData, (state, { payload }) => {
|
488 | return {
|
489 | ...state,
|
490 | data: payload,
|
491 | }
|
492 | })
|
493 | },
|
494 | })
|
495 | return { doNothing, setData, slice }
|
496 | }
|
497 | }
|