Skip to content

Commit eaa0ffc

Browse files
authored
Merge pull request #4544 from EskiMojo14/string-action-type
2 parents 48ef0f4 + 2952422 commit eaa0ffc

File tree

8 files changed

+44
-65
lines changed

8 files changed

+44
-65
lines changed

docs/faq/Actions.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@ sidebar_label: Actions
2323

2424
## Actions
2525

26-
### Why should `type` be a string, or at least serializable? Why should my action types be constants?
26+
### Why should `type` be a string? Why should my action types be constants?
2727

2828
As with state, serializable actions enable several of Redux's defining features, such as time travel debugging, and recording and replaying actions. Using something like a `Symbol` for the `type` value or using `instanceof` checks for actions themselves would break that. Strings are serializable and easily self-descriptive, and so are a better choice. Note that it _is_ okay to use Symbols, Promises, or other non-serializable values in an action if the action is intended for use by middleware. Actions only need to be serializable by the time they actually reach the store and are passed to the reducers.
2929

30-
We can't reliably enforce serializable actions for performance reasons, so Redux only checks that every action is a plain object, and that the `type` is defined. The rest is up to you, but you might find that keeping everything serializable helps debug and reproduce issues.
30+
We can't reliably enforce serializable actions for performance reasons, so Redux only checks that every action is a plain object, and that the `type` is a string. The rest is up to you, but you might find that keeping everything serializable helps debug and reproduce issues.
3131

3232
Encapsulating and centralizing commonly used pieces of code is a key concept in programming. While it is certainly possible to manually create action objects everywhere, and write each `type` value by hand, defining reusable constants makes maintaining code easier. If you put constants in a separate file, you can [check your `import` statements against typos](https://www.npmjs.com/package/eslint-plugin-import) so you can't accidentally use the wrong string.
3333

docs/tutorials/fundamentals/part-7-standard-patterns.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -651,7 +651,7 @@ Here's what the app looks like with that loading status enabled (to see the spin
651651
652652
## Flux Standard Actions
653653

654-
The Redux store itself does not actually care what fields you put into your action object. It only cares that `action.type` exists and has a value, and normal Redux actions always use a string for `action.type`. That means that you _could_ put any other fields into the action that you want. Maybe we could have `action.todo` for a "todo added" action, or `action.color`, and so on.
654+
The Redux store itself does not actually care what fields you put into your action object. It only cares that `action.type` exists and is a string. That means that you _could_ put any other fields into the action that you want. Maybe we could have `action.todo` for a "todo added" action, or `action.color`, and so on.
655655

656656
However, if every action uses different field names for its data fields, it can be hard to know ahead of time what fields you need to handle in each reducer.
657657

src/createStore.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,14 @@ export function createStore<
282282
)
283283
}
284284

285+
if (typeof action.type !== 'string') {
286+
throw new Error(
287+
`Action "type" property must be a string. Instead, the actual type was: '${kindOf(
288+
action.type
289+
)}'. Value was: '${action.type}' (stringified)`
290+
)
291+
}
292+
285293
if (isDispatching) {
286294
throw new Error('Reducers may not dispatch actions.')
287295
}

src/types/actions.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,15 @@
66
*
77
* Actions must have a `type` field that indicates the type of action being
88
* performed. Types can be defined as constants and imported from another
9-
* module. It's better to use strings for `type` than Symbols because strings
10-
* are serializable.
9+
* module. These must be strings, as strings are serializable.
1110
*
1211
* Other than `type`, the structure of an action object is really up to you.
1312
* If you're interested, check out Flux Standard Action for recommendations on
1413
* how actions should be constructed.
1514
*
1615
* @template T the type of the action's `type` tag.
1716
*/
18-
export interface Action<T = any> {
17+
export interface Action<T extends string = string> {
1918
type: T
2019
}
2120

test/combineReducers.spec.ts

Lines changed: 4 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ describe('Utils', () => {
6565

6666
it('throws an error if a reducer returns undefined handling an action', () => {
6767
const reducer = combineReducers({
68-
counter(state: number = 0, action: Action<unknown>) {
68+
counter(state: number = 0, action: Action) {
6969
switch (action && action.type) {
7070
case 'increment':
7171
return state + 1
@@ -95,7 +95,7 @@ describe('Utils', () => {
9595

9696
it('throws an error on first call if a reducer returns undefined initializing', () => {
9797
const reducer = combineReducers({
98-
counter(state: number, action: Action<unknown>) {
98+
counter(state: number, action: Action) {
9999
switch (action.type) {
100100
case 'increment':
101101
return state + 1
@@ -122,23 +122,6 @@ describe('Utils', () => {
122122
).toThrow(/Error thrown in reducer/)
123123
})
124124

125-
it('allows a symbol to be used as an action type', () => {
126-
const increment = Symbol('INCREMENT')
127-
128-
const reducer = combineReducers({
129-
counter(state: number = 0, action: Action<unknown>) {
130-
switch (action.type) {
131-
case increment:
132-
return state + 1
133-
default:
134-
return state
135-
}
136-
}
137-
})
138-
139-
expect(reducer({ counter: 0 }, { type: increment }).counter).toEqual(1)
140-
})
141-
142125
it('maintains referential equality if the reducers it is combining do', () => {
143126
const reducer = combineReducers({
144127
child1(state = {}) {
@@ -161,10 +144,7 @@ describe('Utils', () => {
161144
child1(state = {}) {
162145
return state
163146
},
164-
child2(
165-
state: { count: number } = { count: 0 },
166-
action: Action<unknown>
167-
) {
147+
child2(state: { count: number } = { count: 0 }, action: Action) {
168148
switch (action.type) {
169149
case 'increment':
170150
return { count: state.count + 1 }
@@ -185,7 +165,7 @@ describe('Utils', () => {
185165

186166
it('throws an error on first call if a reducer attempts to handle a private action', () => {
187167
const reducer = combineReducers({
188-
counter(state: number, action: Action<unknown>) {
168+
counter(state: number, action: Action) {
189169
switch (action.type) {
190170
case 'increment':
191171
return state + 1

test/createStore.spec.ts

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import {
44
StoreEnhancer,
55
Action,
66
Store,
7-
Reducer
7+
Reducer,
8+
AnyAction
89
} from 'redux'
910
import { vi } from 'vitest'
1011
import {
@@ -567,17 +568,26 @@ describe('createStore', () => {
567568

568569
it('throws if action type is undefined', () => {
569570
const store = createStore(reducers.todos)
570-
expect(() => store.dispatch({ type: undefined })).toThrow(
571-
/Actions may not have an undefined "type" property/
572-
)
571+
expect(() =>
572+
store.dispatch({ type: undefined } as unknown as AnyAction)
573+
).toThrow(/Actions may not have an undefined "type" property/)
573574
})
574575

575-
it('does not throw if action type is falsy', () => {
576+
it('throws if action type is not string', () => {
576577
const store = createStore(reducers.todos)
577-
expect(() => store.dispatch({ type: false })).not.toThrow()
578-
expect(() => store.dispatch({ type: 0 })).not.toThrow()
579-
expect(() => store.dispatch({ type: null })).not.toThrow()
580-
expect(() => store.dispatch({ type: '' })).not.toThrow()
578+
expect(() =>
579+
store.dispatch({ type: false } as unknown as AnyAction)
580+
).toThrow(/the actual type was: 'boolean'.*Value was: 'false'/)
581+
expect(() => store.dispatch({ type: 0 } as unknown as AnyAction)).toThrow(
582+
/the actual type was: 'number'.*Value was: '0'/
583+
)
584+
expect(() =>
585+
store.dispatch({ type: null } as unknown as AnyAction)
586+
).toThrow(/the actual type was: 'null'.*Value was: 'null'/)
587+
588+
expect(() =>
589+
store.dispatch({ type: '' } as unknown as AnyAction)
590+
).not.toThrow()
581591
})
582592

583593
it('accepts enhancer as the third argument', () => {

test/typescript/actions.ts

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -39,21 +39,3 @@ namespace StringLiteralTypeAction {
3939

4040
const type: ActionType = action.type
4141
}
42-
43-
namespace EnumTypeAction {
44-
enum ActionType {
45-
A,
46-
B,
47-
C
48-
}
49-
50-
interface Action extends ReduxAction {
51-
type: ActionType
52-
}
53-
54-
const action: Action = {
55-
type: ActionType.A
56-
}
57-
58-
const type: ActionType = action.type
59-
}

test/typescript/enhancers.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -159,12 +159,12 @@ function replaceReducerExtender() {
159159
test?: boolean
160160
}
161161

162-
const initialReducer: Reducer<PartialState, Action<unknown>> = () => ({
162+
const initialReducer: Reducer<PartialState, Action> = () => ({
163163
someField: 'string'
164164
})
165165
const store = createStore<
166166
PartialState,
167-
Action<unknown>,
167+
Action,
168168
{ method(): string },
169169
ExtraState
170170
>(initialReducer, enhancer)
@@ -246,10 +246,10 @@ function mhelmersonExample() {
246246
test?: boolean
247247
}
248248

249-
const initialReducer: Reducer<PartialState, Action<unknown>> = () => ({
249+
const initialReducer: Reducer<PartialState, Action> = () => ({
250250
someField: 'string'
251251
})
252-
const store = createStore<PartialState, Action<unknown>, {}, ExtraState>(
252+
const store = createStore<PartialState, Action, {}, ExtraState>(
253253
initialReducer,
254254
enhancer
255255
)
@@ -276,7 +276,7 @@ function finalHelmersonExample() {
276276
foo: string
277277
}
278278

279-
function persistReducer<S, A extends Action<unknown>, PreloadedState>(
279+
function persistReducer<S, A extends Action, PreloadedState>(
280280
config: any,
281281
reducer: Reducer<S, A, PreloadedState>
282282
) {
@@ -300,7 +300,7 @@ function finalHelmersonExample() {
300300
persistConfig: any
301301
): StoreEnhancer<{}, ExtraState> {
302302
return createStore =>
303-
<S, A extends Action<unknown>, PreloadedState>(
303+
<S, A extends Action, PreloadedState>(
304304
reducer: Reducer<S, A, PreloadedState>,
305305
preloadedState?: PreloadedState | undefined
306306
) => {
@@ -323,10 +323,10 @@ function finalHelmersonExample() {
323323
test?: boolean
324324
}
325325

326-
const initialReducer: Reducer<PartialState, Action<unknown>> = () => ({
326+
const initialReducer: Reducer<PartialState, Action> = () => ({
327327
someField: 'string'
328328
})
329-
const store = createStore<PartialState, Action<unknown>, {}, ExtraState>(
329+
const store = createStore<PartialState, Action, {}, ExtraState>(
330330
initialReducer,
331331
createPersistEnhancer('hi')
332332
)

0 commit comments

Comments
 (0)