The return value of createSlice is a Slice type that has a reducer prop with the original State passed in to Reducer.
export declare interface Slice<State = any, CaseReducers extends SliceCaseReducers<State> = SliceCaseReducers<State>> {
/**
* The slice name.
*/
name: string;
/**
* The slice's reducer.
*/
reducer: Reducer<State>; // Why not Reducer<State, SliceAction>??
/**
* Action creators for the types of actions that are handled by the slice
* reducer.
*/
actions: CaseReducerActions<CaseReducers>;
/**
* The individual case reducer functions that were passed in the `reducers` parameter.
* This enables reuse and testing if they were defined inline when calling `createSlice`.
*/
caseReducers: SliceDefinedCaseReducers<CaseReducers>;
}
Can we have the second argument to Reducer passed in as well? Otherwise this reducer (and any others pass into configureStore will mean store.dispatch will accept AnyAction.
Every reducer does always accept any possible action.
That's why every reducer has a default case that returns the original state.
You can also always dispatch every possible action against your store, it just wouldn't do anything if no reducer would listen for it.
Here's a simplified example, but there would be the same error with combineReducers with each reducer type being defined with State and Action types (the store dispatch would then be a union type of all the possible actions, and error if an action doesn't match).
Is there a way to recreate this with redux toolkit? I think this would be the appropriate type checking since you would get instant feedback that your action isn't doing anything.
import { Reducer, createStore, AnyAction } from 'redux';
interface InitAction {
type: 'init',
payload: boolean;
}
interface InitialState {
init: boolean;
}
const initialState = {
init: false
}
const reducer: Reducer<InitialState, InitAction> = (state = initialState, action: InitAction): InitialState => {
if (action.type === 'init') {
return { init: action.payload };
}
return state;
}
const store = createStore(reducer);
store.dispatch({ type: 'init', payload: true });
store.dispatch({ type: 'bad'});
// Type '"bad"' is not assignable to type '"init"'
I personally don't feel like there's a lot of useful value in trying to limit what actions can be passed to dispatch, but there do seem to be a number of people who feel like that's an important / necessary thing.
If you want it to actually contain the type property in the Action, this is impossible.
Action types created with createSlice are concatenations of slice.name and the key of the the reducer.
As TypeScript cannot do string concatenation in types (so type X = 'mySlice' + '/' + 'typeName';) is impossible), we cannot define anything more accurate than string as the type of actions created with createSlice.
If you only want the actions, like { type: string; payload: Every | Possible | Payload }, that's theoretically possible, but I'd say pretty pointless...?
And again: By doing what you suggest, you are lying to TypeScript, and that's not a good thing. The resulting sliceReducer will definitely be called by actions for other slices, so returning a Reducer<State, OnlyThisSliceAction> would be returning an incorrect type.
If you only want the actions, like
{ type: string; payload: Every | Possible | Payload }, that's theoretically possible, but I'd say pretty pointless...?
It wouldn't be all possible payloads in each action, it would be all possible actions (union type) as input for the dispatch function.
It seems the Reducer type in both React (with context api) and redux support this, but not the toolkit. I don't think it would add a ton of value as Mark said, but would make it type safe if someone did happen to use an unsupported action type as an argument to the dispatch function.
And great work with the library in general, I think it will be a game changer.
I think you're coming at it from the wrong direction.
The type-safety goal there isn't to prevent the "wrong" actions from being passed to the _reducer_. It's to prevent the actions from being passed to dispatch().
I think what you're actually looking for here is to combine all the possible action types, and then do something like:
export type AppDispatch = Dispatch<AllActionTypesCombined>;
// later, in a component:
import {AppDispatch} from "app/store";
// component
const dispatch: AppDispatch = useDispatch();
Yes exactly, sorry if I was unclear.
I have a custom hook: const useAppDispatch = () => useDispatch<AllActionTypesCombined>();
It's easy to have AllActionTypesCombined with context api and/or redux as it's part of my types already, but it would be nice to have access to AllSliceActions from the Slice type created by createSlice.
Something like:
const someSlice = createSlice({ ... });
type SomeSliceActions = typeof someSlice.actions; // this is the action creators
type AllActionTypesCombined = SomeSliceActions | SomeOtherSliceActions | ...;
or even better (which is why I mentioned having the actions put on the slice Reducer):
// rootReducer would presumably have AllActionTypesCombined
const rootReducer = combineReducers({ ... });
type AllActionTypesCombined = typeof rootReducer.actions // Get it from here??
or best:
export const useAppDispatch = () => useDispatch<typeof store.dispatch>();
// current type is: ThunkDispatch<CombinedState<AppState>, undefined, AnyAction> & Dispatch<AnyAction>
// would want: ThunkDispatch<CombinedState<AppState>, undefined, AllActionTypesCombined> & Dispatch<AllActionTypesCombined>
Let me know if I'm missing something easy.
Okay, above I meant
type SliceActions = { type: string; payload: Every } | { type: string; payload: Possible } | { type: string; payload: Payload };
for TS, that is essentially equivalent to what I wrote.
The point is: type will always be string, and it is not possible in TypeScript to infer or for us to validate/specify that type - as string literals cannot be concatenated on a type level.
With type always being string, this will possibly not be of any value for you.
I also have this problem. I think I'm going to use createReducer instead
const increment = createAction<number, "counter/increment">("counter/increment");
const decrement = createAction<number, "counter/decrement">("counter/decrement");
const counter = createReducer(0, builder => builder
.addCase(increment, (state, action) => state + action.payload)
.addCase(decrement, (state, action) => state - action.payload)
);
type MyActions = ReturnType<typeof increment | typeof decrement>;
Having to do createAction is a little more boilerplate but I don't really see a way around it
@phryneas, @markerikson and @evertbouw thank you for the responses. Very happy to have something to reduce the redux complexity and looking forward to future updates.
Most helpful comment
I also have this problem. I think I'm going to use createReducer instead
Having to do
createActionis a little more boilerplate but I don't really see a way around it