| 1 | import type {
|
|---|
| 2 | Action,
|
|---|
| 3 | ActionCreator,
|
|---|
| 4 | ActionCreatorWithNonInferrablePayload,
|
|---|
| 5 | ActionCreatorWithOptionalPayload,
|
|---|
| 6 | ActionCreatorWithPayload,
|
|---|
| 7 | ActionCreatorWithPreparedPayload,
|
|---|
| 8 | ActionCreatorWithoutPayload,
|
|---|
| 9 | PayloadAction,
|
|---|
| 10 | PayloadActionCreator,
|
|---|
| 11 | UnknownAction,
|
|---|
| 12 | } from '@reduxjs/toolkit'
|
|---|
| 13 | import { createAction } from '@reduxjs/toolkit'
|
|---|
| 14 |
|
|---|
| 15 | describe('type tests', () => {
|
|---|
| 16 | describe('PayloadAction', () => {
|
|---|
| 17 | test('PayloadAction has type parameter for the payload.', () => {
|
|---|
| 18 | const action: PayloadAction<number> = { type: '', payload: 5 }
|
|---|
| 19 |
|
|---|
| 20 | expectTypeOf(action.payload).toBeNumber()
|
|---|
| 21 |
|
|---|
| 22 | expectTypeOf(action.payload).not.toBeString()
|
|---|
| 23 | })
|
|---|
| 24 |
|
|---|
| 25 | test('PayloadAction type parameter is required.', () => {
|
|---|
| 26 | expectTypeOf({ type: '', payload: 5 }).not.toMatchTypeOf<PayloadAction>()
|
|---|
| 27 | })
|
|---|
| 28 |
|
|---|
| 29 | test('PayloadAction has a string type tag.', () => {
|
|---|
| 30 | expectTypeOf({ type: '', payload: 5 }).toEqualTypeOf<
|
|---|
| 31 | PayloadAction<number>
|
|---|
| 32 | >()
|
|---|
| 33 |
|
|---|
| 34 | expectTypeOf({ type: 1, payload: 5 }).not.toMatchTypeOf<PayloadAction>()
|
|---|
| 35 | })
|
|---|
| 36 |
|
|---|
| 37 | test('PayloadAction is compatible with Action<string>', () => {
|
|---|
| 38 | const action: PayloadAction<number> = { type: '', payload: 5 }
|
|---|
| 39 |
|
|---|
| 40 | expectTypeOf(action).toMatchTypeOf<Action<string>>()
|
|---|
| 41 | })
|
|---|
| 42 | })
|
|---|
| 43 |
|
|---|
| 44 | describe('PayloadActionCreator', () => {
|
|---|
| 45 | test('PayloadActionCreator returns correctly typed PayloadAction depending on whether a payload is passed.', () => {
|
|---|
| 46 | const actionCreator = Object.assign(
|
|---|
| 47 | (payload?: number) => ({
|
|---|
| 48 | type: 'action',
|
|---|
| 49 | payload,
|
|---|
| 50 | }),
|
|---|
| 51 | { type: 'action' },
|
|---|
| 52 | ) as PayloadActionCreator<number | undefined>
|
|---|
| 53 |
|
|---|
| 54 | expectTypeOf(actionCreator(1)).toEqualTypeOf<
|
|---|
| 55 | PayloadAction<number | undefined>
|
|---|
| 56 | >()
|
|---|
| 57 |
|
|---|
| 58 | expectTypeOf(actionCreator()).toEqualTypeOf<
|
|---|
| 59 | PayloadAction<number | undefined>
|
|---|
| 60 | >()
|
|---|
| 61 |
|
|---|
| 62 | expectTypeOf(actionCreator(undefined)).toEqualTypeOf<
|
|---|
| 63 | PayloadAction<number | undefined>
|
|---|
| 64 | >()
|
|---|
| 65 |
|
|---|
| 66 | expectTypeOf(actionCreator()).not.toMatchTypeOf<PayloadAction<number>>()
|
|---|
| 67 |
|
|---|
| 68 | expectTypeOf(actionCreator(1)).not.toMatchTypeOf<
|
|---|
| 69 | PayloadAction<undefined>
|
|---|
| 70 | >()
|
|---|
| 71 | })
|
|---|
| 72 |
|
|---|
| 73 | test('PayloadActionCreator is compatible with ActionCreator.', () => {
|
|---|
| 74 | const payloadActionCreator = Object.assign(
|
|---|
| 75 | (payload?: number) => ({
|
|---|
| 76 | type: 'action',
|
|---|
| 77 | payload,
|
|---|
| 78 | }),
|
|---|
| 79 | { type: 'action' },
|
|---|
| 80 | ) as PayloadActionCreator
|
|---|
| 81 |
|
|---|
| 82 | expectTypeOf(payloadActionCreator).toMatchTypeOf<
|
|---|
| 83 | ActionCreator<UnknownAction>
|
|---|
| 84 | >()
|
|---|
| 85 |
|
|---|
| 86 | const payloadActionCreator2 = Object.assign(
|
|---|
| 87 | (payload?: number) => ({
|
|---|
| 88 | type: 'action',
|
|---|
| 89 | payload: payload || 1,
|
|---|
| 90 | }),
|
|---|
| 91 | { type: 'action' },
|
|---|
| 92 | ) as PayloadActionCreator<number>
|
|---|
| 93 |
|
|---|
| 94 | expectTypeOf(payloadActionCreator2).toMatchTypeOf<
|
|---|
| 95 | ActionCreator<PayloadAction<number>>
|
|---|
| 96 | >()
|
|---|
| 97 | })
|
|---|
| 98 | })
|
|---|
| 99 |
|
|---|
| 100 | test('createAction() has type parameter for the action payload.', () => {
|
|---|
| 101 | const increment = createAction<number, 'increment'>('increment')
|
|---|
| 102 |
|
|---|
| 103 | expectTypeOf(increment).parameter(0).toBeNumber()
|
|---|
| 104 |
|
|---|
| 105 | expectTypeOf(increment).parameter(0).not.toBeString()
|
|---|
| 106 | })
|
|---|
| 107 |
|
|---|
| 108 | test('createAction() type parameter is required, not inferred (defaults to `void`).', () => {
|
|---|
| 109 | const increment = createAction('increment')
|
|---|
| 110 |
|
|---|
| 111 | expectTypeOf(increment).parameter(0).not.toBeNumber()
|
|---|
| 112 |
|
|---|
| 113 | expectTypeOf(increment().payload).not.toBeNumber()
|
|---|
| 114 | })
|
|---|
| 115 |
|
|---|
| 116 | test('createAction().type is a string literal.', () => {
|
|---|
| 117 | const increment = createAction<number, 'increment'>('increment')
|
|---|
| 118 |
|
|---|
| 119 | expectTypeOf(increment(1).type).toBeString()
|
|---|
| 120 |
|
|---|
| 121 | expectTypeOf(increment(1).type).toEqualTypeOf<'increment'>()
|
|---|
| 122 |
|
|---|
| 123 | expectTypeOf(increment(1).type).not.toMatchTypeOf<'other'>()
|
|---|
| 124 |
|
|---|
| 125 | expectTypeOf(increment(1).type).not.toBeNumber()
|
|---|
| 126 | })
|
|---|
| 127 |
|
|---|
| 128 | test('type still present when using prepareAction', () => {
|
|---|
| 129 | const strLenAction = createAction('strLen', (payload: string) => ({
|
|---|
| 130 | payload: payload.length,
|
|---|
| 131 | }))
|
|---|
| 132 |
|
|---|
| 133 | expectTypeOf(strLenAction('test').type).toBeString()
|
|---|
| 134 | })
|
|---|
| 135 |
|
|---|
| 136 | test('changing payload type with prepareAction', () => {
|
|---|
| 137 | const strLenAction = createAction('strLen', (payload: string) => ({
|
|---|
| 138 | payload: payload.length,
|
|---|
| 139 | }))
|
|---|
| 140 |
|
|---|
| 141 | expectTypeOf(strLenAction('test').payload).toBeNumber()
|
|---|
| 142 |
|
|---|
| 143 | expectTypeOf(strLenAction('test').payload).not.toBeString()
|
|---|
| 144 |
|
|---|
| 145 | expectTypeOf(strLenAction('test')).not.toHaveProperty('error')
|
|---|
| 146 | })
|
|---|
| 147 |
|
|---|
| 148 | test('adding metadata with prepareAction', () => {
|
|---|
| 149 | const strLenMetaAction = createAction('strLenMeta', (payload: string) => ({
|
|---|
| 150 | payload,
|
|---|
| 151 | meta: payload.length,
|
|---|
| 152 | }))
|
|---|
| 153 |
|
|---|
| 154 | expectTypeOf(strLenMetaAction('test').meta).toBeNumber()
|
|---|
| 155 |
|
|---|
| 156 | expectTypeOf(strLenMetaAction('test').meta).not.toBeString()
|
|---|
| 157 |
|
|---|
| 158 | expectTypeOf(strLenMetaAction('test')).not.toHaveProperty('error')
|
|---|
| 159 | })
|
|---|
| 160 |
|
|---|
| 161 | test('adding boolean error with prepareAction', () => {
|
|---|
| 162 | const boolErrorAction = createAction('boolError', (payload: string) => ({
|
|---|
| 163 | payload,
|
|---|
| 164 | error: true,
|
|---|
| 165 | }))
|
|---|
| 166 |
|
|---|
| 167 | expectTypeOf(boolErrorAction('test').error).toBeBoolean()
|
|---|
| 168 |
|
|---|
| 169 | expectTypeOf(boolErrorAction('test').error).not.toBeString()
|
|---|
| 170 | })
|
|---|
| 171 |
|
|---|
| 172 | test('adding string error with prepareAction', () => {
|
|---|
| 173 | const strErrorAction = createAction('strError', (payload: string) => ({
|
|---|
| 174 | payload,
|
|---|
| 175 | error: 'this is an error',
|
|---|
| 176 | }))
|
|---|
| 177 |
|
|---|
| 178 | expectTypeOf(strErrorAction('test').error).toBeString()
|
|---|
| 179 |
|
|---|
| 180 | expectTypeOf(strErrorAction('test').error).not.toBeBoolean()
|
|---|
| 181 | })
|
|---|
| 182 |
|
|---|
| 183 | test('regression test for https://github.com/reduxjs/redux-toolkit/issues/214', () => {
|
|---|
| 184 | const action = createAction<{ input?: string }>('ACTION')
|
|---|
| 185 |
|
|---|
| 186 | expectTypeOf(action({ input: '' }).payload.input).toEqualTypeOf<
|
|---|
| 187 | string | undefined
|
|---|
| 188 | >()
|
|---|
| 189 |
|
|---|
| 190 | expectTypeOf(action({ input: '' }).payload.input).not.toBeNumber()
|
|---|
| 191 |
|
|---|
| 192 | expectTypeOf(action).parameter(0).not.toMatchTypeOf({ input: 3 })
|
|---|
| 193 | })
|
|---|
| 194 |
|
|---|
| 195 | test('regression test for https://github.com/reduxjs/redux-toolkit/issues/224', () => {
|
|---|
| 196 | const oops = createAction('oops', (x: any) => ({
|
|---|
| 197 | payload: x,
|
|---|
| 198 | error: x,
|
|---|
| 199 | meta: x,
|
|---|
| 200 | }))
|
|---|
| 201 |
|
|---|
| 202 | expectTypeOf(oops('').payload).toBeAny()
|
|---|
| 203 |
|
|---|
| 204 | expectTypeOf(oops('').error).toBeAny()
|
|---|
| 205 |
|
|---|
| 206 | expectTypeOf(oops('').meta).toBeAny()
|
|---|
| 207 | })
|
|---|
| 208 |
|
|---|
| 209 | describe('createAction.match()', () => {
|
|---|
| 210 | test('simple use case', () => {
|
|---|
| 211 | const actionCreator = createAction<string, 'test'>('test')
|
|---|
| 212 |
|
|---|
| 213 | const x: Action<string> = {} as any
|
|---|
| 214 |
|
|---|
| 215 | if (actionCreator.match(x)) {
|
|---|
| 216 | expectTypeOf(x.type).toEqualTypeOf<'test'>()
|
|---|
| 217 |
|
|---|
| 218 | expectTypeOf(x.payload).toBeString()
|
|---|
| 219 | } else {
|
|---|
| 220 | expectTypeOf(x.type).not.toMatchTypeOf<'test'>()
|
|---|
| 221 |
|
|---|
| 222 | expectTypeOf(x).not.toHaveProperty('payload')
|
|---|
| 223 | }
|
|---|
| 224 | })
|
|---|
| 225 |
|
|---|
| 226 | test('special case: optional argument', () => {
|
|---|
| 227 | const actionCreator = createAction<string | undefined, 'test'>('test')
|
|---|
| 228 |
|
|---|
| 229 | const x: Action<string> = {} as any
|
|---|
| 230 |
|
|---|
| 231 | if (actionCreator.match(x)) {
|
|---|
| 232 | expectTypeOf(x.type).toEqualTypeOf<'test'>()
|
|---|
| 233 |
|
|---|
| 234 | expectTypeOf(x.payload).toEqualTypeOf<string | undefined>()
|
|---|
| 235 | }
|
|---|
| 236 | })
|
|---|
| 237 |
|
|---|
| 238 | test('special case: without argument', () => {
|
|---|
| 239 | const actionCreator = createAction('test')
|
|---|
| 240 |
|
|---|
| 241 | const x: Action<string> = {} as any
|
|---|
| 242 |
|
|---|
| 243 | if (actionCreator.match(x)) {
|
|---|
| 244 | expectTypeOf(x.type).toEqualTypeOf<'test'>()
|
|---|
| 245 |
|
|---|
| 246 | expectTypeOf(x.payload).not.toMatchTypeOf<{}>()
|
|---|
| 247 | }
|
|---|
| 248 | })
|
|---|
| 249 |
|
|---|
| 250 | test('special case: with prepareAction', () => {
|
|---|
| 251 | const actionCreator = createAction('test', () => ({
|
|---|
| 252 | payload: '',
|
|---|
| 253 | meta: '',
|
|---|
| 254 | error: false,
|
|---|
| 255 | }))
|
|---|
| 256 |
|
|---|
| 257 | const x: Action<string> = {} as any
|
|---|
| 258 |
|
|---|
| 259 | if (actionCreator.match(x)) {
|
|---|
| 260 | expectTypeOf(x.type).toEqualTypeOf<'test'>()
|
|---|
| 261 |
|
|---|
| 262 | expectTypeOf(x.payload).toBeString()
|
|---|
| 263 |
|
|---|
| 264 | expectTypeOf(x.meta).toBeString()
|
|---|
| 265 |
|
|---|
| 266 | expectTypeOf(x.error).toBeBoolean()
|
|---|
| 267 |
|
|---|
| 268 | expectTypeOf(x.payload).not.toBeNumber()
|
|---|
| 269 |
|
|---|
| 270 | expectTypeOf(x.meta).not.toBeNumber()
|
|---|
| 271 |
|
|---|
| 272 | expectTypeOf(x.error).not.toBeNumber()
|
|---|
| 273 | }
|
|---|
| 274 | })
|
|---|
| 275 | test('potential use: as array filter', () => {
|
|---|
| 276 | const actionCreator = createAction<string, 'test'>('test')
|
|---|
| 277 |
|
|---|
| 278 | const x: Action<string>[] = []
|
|---|
| 279 |
|
|---|
| 280 | expectTypeOf(x.filter(actionCreator.match)).toEqualTypeOf<
|
|---|
| 281 | PayloadAction<string, 'test'>[]
|
|---|
| 282 | >()
|
|---|
| 283 | })
|
|---|
| 284 | })
|
|---|
| 285 |
|
|---|
| 286 | test('ActionCreatorWithOptionalPayload', () => {
|
|---|
| 287 | expectTypeOf(createAction<string | undefined>('')).toEqualTypeOf<
|
|---|
| 288 | ActionCreatorWithOptionalPayload<string | undefined>
|
|---|
| 289 | >()
|
|---|
| 290 |
|
|---|
| 291 | expectTypeOf(
|
|---|
| 292 | createAction<void>(''),
|
|---|
| 293 | ).toEqualTypeOf<ActionCreatorWithoutPayload>()
|
|---|
| 294 |
|
|---|
| 295 | assertType<ActionCreatorWithNonInferrablePayload>(createAction(''))
|
|---|
| 296 |
|
|---|
| 297 | expectTypeOf(createAction<string>('')).toEqualTypeOf<
|
|---|
| 298 | ActionCreatorWithPayload<string>
|
|---|
| 299 | >()
|
|---|
| 300 |
|
|---|
| 301 | expectTypeOf(
|
|---|
| 302 | createAction('', (_: 0) => ({
|
|---|
| 303 | payload: 1 as 1,
|
|---|
| 304 | error: 2 as 2,
|
|---|
| 305 | meta: 3 as 3,
|
|---|
| 306 | })),
|
|---|
| 307 | ).toEqualTypeOf<ActionCreatorWithPreparedPayload<[0], 1, '', 2, 3>>()
|
|---|
| 308 |
|
|---|
| 309 | const anyCreator = createAction<any>('')
|
|---|
| 310 |
|
|---|
| 311 | expectTypeOf(anyCreator).toEqualTypeOf<ActionCreatorWithPayload<any>>()
|
|---|
| 312 |
|
|---|
| 313 | expectTypeOf(anyCreator({}).payload).toBeAny()
|
|---|
| 314 | })
|
|---|
| 315 |
|
|---|
| 316 | test("Verify action creators should not be passed directly as arguments to React event handlers if there shouldn't be a payload", () => {
|
|---|
| 317 | const emptyAction = createAction<void>('empty/action')
|
|---|
| 318 |
|
|---|
| 319 | function TestComponent() {
|
|---|
| 320 | // This typically leads to an error like:
|
|---|
| 321 | // // A non-serializable value was detected in an action, in the path: `payload`.
|
|---|
| 322 | // @ts-expect-error Should error because `void` and `MouseEvent` aren't compatible
|
|---|
| 323 | return <button onClick={emptyAction}>+</button>
|
|---|
| 324 | }
|
|---|
| 325 | })
|
|---|
| 326 | })
|
|---|