Hi,
I seem to be searching for the wrong terms, is there some better way to get action types from a slice instead of doing
ReturnType<typeof slice.actionName>?
What do you mean by "get the action types"? What are you trying to accomplish?
I'd strongly suggest against extracting these types and doing anything with them. Use actionCreator.match as a typeguard where you need it, you won't need them anywhere else.
I'll just copy & paste a rant on the topic I recently had in the reactiflux discord on this. Wanted to make a blog article out of it, but didn't get to it yet - but maybe it is insightful your you in it's raw form as well.
user:
can anyone point me to documentation showing how explicitly typing combineReducers with Reduceris bad practice
me: I don't believe such documentation exists, but if it existed it would probably be me to have written it.
createReducer already returns a correctly returned reducer. Manually typing that can mean three things:
- you want to tighten that type definition. In that case you have to as any it before declaring a stricter type. After that, any kind of error & mistake can happen and you'll never notice it, as you have any-casted it on the way.
- you manually specify the exact type it already has. If it changes in the future, one of the other two cases applies until you update it manually back to this state.
- you manually specify a type that is looser than it was before, losing type information
So going from there - what exactly do you want to do by typing it manually?
user:
@phryneas telling team members why it's a bad idea
me:
Okay, let's go a little further into it ^^
A reducer has two generic arguments and this signature:
export type Reducer<S = any, A extends Action = AnyAction> = (
state: S | undefined,
action: A
) => S
So you specify the state type and the action type.
Now, the state type is something that you can also already specify in createReducer by just having initialState typed correctly. But createReducer will always return a state with an argument of AnyAction.
Why is that?
It's pretty simple: every reducer takes every action. If you were to specify an action type there and would write the reducer in a way that assumed that only those action types will be passed in, you would even have an invalid reducer.
Assume this reducer:
const reducer: Reducer<string, { type: "MyAction", payload: { foo: string} }}> = (state, action) => {
return action.payload.foo
}
this is 100% correct going by it's typings. But it will crash as soon as your store is created.
Using that type introduced a lie for the compiler: that this reducer will only ever be called with a specific action. But actually, it will be called with every action ever dispatched in your application PLUS all actions dispatched from middlewares PLUS an "INIT" action dispatched by redux on intialization - in dev mode that one even has a random type.
So the only way of really "typing the reality of a reducer" is by typing Reducer. If you were doing it differently, TypeScript might forget to warn you about a missing default return value or hide bugs like I provoked above.
And that's why createReducer returns Reducerand nothing else.
And manually typing that will either keep it at exact that same level & introduce manual labor, or make it worse in one way or another.
Pratical example with redux-saga would be:
const action: TypeX = yield take(slice.action.match)
I could do
const action = yield take(slice.action.match)
if (slice.action.match(action) {
}
but that is relatively ugly and I would prefer to just have TypeX there. So I was wondering if there is a nice way to have TypeX.
@ksjogo You probably just want to use typed-redux-saga instead of redux-saga, there's not much need to specify that by hand:
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { take } from "typed-redux-saga";
const slice = createSlice({
name: "testSlice",
initialState: {
value: 0
},
reducers: {
incrementBy(state, action: PayloadAction<number>) {
state.value += action.payload;
}
}
});
function* exampleSaga() {
/**
* correctly inferred as
const action: {
payload: number;
type: string;
}
*/
const action = yield* take(slice.actions.incrementBy);
}
@phryneas Being able to extract the types is useful for a different style of writing type safe reducers or middleware.
If you're using the style of
function reducer(state, action: DefinedActions) {
switch (action.type) {
case DEFINED_ACTION_A_TYPE:
// TypeScript has narrowed the type to DefinedActionA
case DEFINED_ACTION_B_TYPE:
// TypeScript has narrowed the type to DefinedActionB
}
Then DefinedActions is usually defined as a union of all the actions defined in the system.
I realize that redux-toolkit has its own reducer mechanism that uses a different style, but supporting type extraction would assist with transitioning from non-redux-toolkit to redux-toolkit implementations, and also, in a middleware where I might also want to handle a large number of types, I would prefer not to use a large number of if (myAction.match(type)) statements.
Per Lenz's comments, we recommend _not_ writing reducers that way :) createSlice already handles the "switch" aspect, generates the types and action creators for the actions this slice defines itself, and provides inference in the extraReducers builder callback for action creators that were defined in other slices. That way there's no need to "narrow" any types.
If you want to match against one of several possible action types with RTK's action creators, you could always do something like:
const actionMatches = (action, actionCreators) => actionCreators.some(ac => ac.match(action));
// later
if (actionMatches(action, [addTodo, toggleTodo]) {
}
@markerikson Yes, I realize that for the reducers case you've already handled it, and I don't see a reason to keep reducers in a different style. However, there might be other use cases where the action types are useful to have.
However, I think this is a solved problem. @ksjogo had the solution from the beginning, it just looks somewhat ugly.
@ksjogo If you do have a use for this that makes sense, you can just create a generic helper type for yourself, similar to how ReturnType works, but at a higher level.
@douglas-treadwell please read my comment above again. Your code is a lie to the compiler: it tells the compiler that that function will only ever be called with specific actions.
That is like writing a function foo(arg: string) {}, knowing full-well that foo will also be called with number as arg, but just not caring about it. Sooner or later, someone will trust that method signature and try to access arg.length.
Using a discriminated union there will lead to TypeScript not warning you about potential bugs like missing return statements, because the type that was passed in is already exhausted after two switch statements.
It will not warn you about deeply nested object property access on undefined object members if all the actions you passed in accidentally share a shape too well.
It will lead to team members reading that code to the assumption that that code will only be called with these specific actions - which it will not. It will also be called by every other action.
I really recommend not doing that. Disciminated unions are a nice pattern if you really know every possible case, but for redux actions, they have always been a crutch, not a good solution.
@phryneas Yes, I fully understand what you've said. That style does have certain risks. As you said, it always has been a sort of crutch, trading a lie to the compiler to save time writing type guards, and that's definitely not an ideal solution. With redux-toolkit, it seems that the situations in which one would think to look for a crutch are dramatically reduced. Among other reasons, because type guards are available for free. And the toolkit likely saves enough time that time remains to do things in a more ideal way in the context-specific situations that remain.
By the way, thanks very much to the creators and contributors (such as @phryneas and @markerikson) for creating this library. It's saving me a lot of time. Also, I appreciate your time discussing it here in the Issues. Please point me to the tip jar if you have one.
@douglas-treadwell We don't. But we maybe should make a list of some good causes to donate to instead ;)
I'd suggest the EFF, your local equivalent - or just any good cause in your community :) Will definitely give us a good feeling knowing we inspired you to do that ;) Any amount always helps!
FYI, donated $25 to EFF in honor of redux-toolkit.

@markerikson
One issue with the actionMatches(...) approach is that it doesn't act as a type guard the way match(...) does.
I could attempt to make it a type guard, but then I would write...
function actionMatches(action, actionCreators): action is a What { }
And it seems I would need to extract the type to determine what types What is a union of.
Maybe I'm just not seeing a much simpler solution. Or if there isn't a simpler solution, I suppose a multi-action matcher like this could be added to RTK, such that any type extraction is hidden within the library.
Thanks again for taking the time to discuss this.
@phryneas y'know, given that we keep telling people to write this util... I'm inclined to agree we ought to just write it ourselves and include it :)
Not sure how to handle the determination of what the action type is as a result, though.
Wasn't aware of typed-redux-saga, will check it out, thanks for the pointer.
@douglas-treadwell very cool! :blush:
@douglas-treadwell @markerikson What would you think about more generic or and and functions to work with type guards?
type Matcher<T> = { match(v: any): v is T } | ((v: any) => v is T)
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never
type Or<Conditions extends [Matcher<any>, ...Matcher<any>[]]> = Conditions[number] extends infer C ? C extends Matcher<infer T> ? T : never : never;
type And<Conditions extends [Matcher<any>, ...Matcher<any>[]]> = UnionToIntersection<Or<Conditions>>
declare function or<Conditions extends [Matcher<any>, ...Matcher<any>[]]>(...conditions: Conditions): (v: any) => v is Or<Conditions>;
declare function and<Conditions extends [Matcher<any>, ...Matcher<any>[]]>(...conditions: Conditions): (v: any) => v is And<Conditions>;
I'm simultaneously:
But yeah, I think that seems like it would solve the use case pretty well.
We could of course also call them some and every to hide the fact that we reinvent boolean logic there :laughing:
PS: or oneOf/someOf and allOf
Slightly more seriously: what's the point of the and function here?
Hmm. I guess I was assuming we'd be limiting the matchers to purely addTodo.match, etc, but I guess it could be something like (action => action.type.startsWith('todos'), action => action.type.endsWith('pending')).
Okay, next item: we can't pass the action creators themselves straight to those matchers right now, can we? It has to be or(addTodo.match, toggleTodo.match). Can we come up with a way to pass the action creators straight in to make this really easy to say "any one of these"?
While we're brainstorming possible solutions, here's another option.
type Matcher<T> = { match(v: any): v is T } | ((v: any) => v is T);
type ActionFromMatcher<T extends Matcher<any>> = T extends Matcher<infer R> ? R : never;
function match<T extends Array<Matcher<any>>> (action: any, matchers: T): action is ActionFromMatcher<typeof matchers[number]> {
return matchers.some((matcher) => {
if (typeof matcher === 'function') {
return matcher(action);
} else {
return matcher.match(action);
}
});
}
This appears to work.
Example usage:
const asyncThunk1 = createAsyncThunk<{prop1: number, prop3: number}>(
'asyncThunk1',
async () => {
return {
prop1: 1,
prop3: 3
};
}
);
const asyncThunk2 = createAsyncThunk<{prop1: number, prop2: number}>(
'asyncThunk2',
async () => {
return {
prop1: 1,
prop2: 2
};
}
);
function test(action: AnyAction) {
if (match(action, [asyncThunk1.fulfilled, asyncThunk2.fulfilled])) {
return {
prop1: action.payload.prop1, // OK
prop2: action.payload.prop2, // ERROR (as expected)
prop3: action.payload.prop3 // ERROR (as expected)
};
}
}
@markerikson
Re: Can we come up with a way to pass the action creators straight in to make this really easy to say "any one of these"?
type Matcher<T> = { match(v: any): v is T } | ((v: any) => v is T);
type ActionFromMatcher<T extends Matcher<any>> = T extends Matcher<infer R> ? R : never;
type ActionsFromAsyncThunk<T extends AsyncThunk<any, any, any>> = (
ActionFromMatcher<T['pending']> |
ActionFromMatcher<T['fulfilled']> |
ActionFromMatcher<T['rejected']>
);
type ActionFromMatcherOrThunk<T extends (Matcher<any> | AsyncThunk<any, any, any>)> =
T extends Matcher<any> ? ActionFromMatcher<T> :
T extends AsyncThunk<any, any, any> ? ActionsFromAsyncThunk<T>:
never;
function isAsyncThunk(arg: any): arg is AsyncThunk<any, any, any> {
// I'm being quick about writing this
try {
if (arg as AsyncThunk<any, any, any>) {
return arg.pending && arg.fulfilled && arg.rejected;
}
} catch {
return false;
}
return false;
}
function match<T extends Array<Matcher<any> | AsyncThunk<any, any, any>>> (action: any, matchers: T): action is ActionFromMatcherOrThunk<typeof matchers[number]> {
return matchers.some((matcher) => {
if (isAsyncThunk(matcher)) {
return matcher.pending.match(action) || matcher.fulfilled.match(action) || matcher.rejected.match(action);
} else if (typeof matcher === 'function') {
return matcher(action);
} else {
return matcher.match(action);
}
});
}
The match on pending || fulfilled || rejected may be useful in some cases, but it might also be useful to define matchPending, matchFulfilled, and matchRejected based on the above, but filtering only the pending, fulfilled, or rejected action type of each action creator.
For example, we can add...
type ActionFromMatcherOrFulfilledThunk<T extends (Matcher<any> | AsyncThunk<any, any, any>)> =
T extends Matcher<any> ? ActionFromMatcher<T> :
T extends AsyncThunk<any, any, any> ? ActionFromMatcher<T['fulfilled']>:
never;
function matchFulfilled<T extends Array<Matcher<any> | AsyncThunk<any, any, any>>> (action: any, matchers: T): action is ActionFromMatcherOrFulfilledThunk<typeof matchers[number]> {
return matchers.some((matcher) => {
if (isAsyncThunk(matcher)) {
return matcher.fulfilled.match(action);
} else if (typeof matcher === 'function') {
return matcher(action);
} else {
return matcher.match(action);
}
});
}
And this example function seems to work as expected:
function test(action: AnyAction) {
if (matchFulfilled(action, [asyncThunk1, asyncThunk2])) {
return {
prop1: action.payload.prop1, // OK
prop2: action.payload.prop2, // ERROR (as expected)
prop3: action.payload.prop3 // ERROR (as expected)
};
}
}
Y'all are making me feel like I don't know any TS at all over here :)
These look pretty promising at first glance.
@phryneas I think the and() and or() functions are excellent to have for the general case of combining several type guards, which I'm sure I will use. In redux-toolkit specific situations I am hoping to not need to write my own, possibly very long combinations of and() or or(), so I'd lean toward something closer to match(action, actionCreators) in the form @markerikson suggested (with example implementation above). Although now that I think about it further, I suppose I will need to create the actionCreators array either way.
@markerikson see type Matcher<T> type Matcher<T> = { match(v: any): v is T } | ((v: any) => v is T)= { match(v: any): v is T } | ((v: any) => v is T) - that will work with action creators as well as normal type guard functions
@douglas-treadwell thing is, a match(action, actionCreators) will be able to be used with external libraries, but not with RTK itself. The only position where RTK accepts a typeguard is builder.addMatcher - and the signature is just matcher(action) there. This is why I wrote or/and as higher order functions.
See:
const actionA = createAction<string>('a');
const actionB = createAction<string>('b');
createReducer({}, builder => builder
.addMatcher(or(actionA, actionB), (state, action) => {
}))
for usage outside of RTK, the other signature might be nicer - although it might be a nice convention anyways to do something like
const isImportantAction = or(actionA, actionB);
if (isImportantAction(x)) {
}
Also, having that actions array as a parameter could prove problematic, because depending on how the user decides to cobble that array together, they will easily lose type info.(TypeScript will very likely at some point start to loosen/consolidate the type from Array<ActionCreatorWithPayload<A> | ActionCreatorWithPayload<B>> to Array<ActionCreatorWithPayload<unknown>>) as we have experienced e.g. with middlewares.
I like the idea of a matchFulfilled etc, but I'd also go for a HOF signature with that. Bonus point: matchFulfilled() without arguments could return a matcher that would match for a generic "fulfilled" shape.
@douglas-treadwell would you be willing to do a PR for this topic? We'll sure have some more discussions on the road, but I think in general this would be a very helpful addition :)
@phryneas Of course, I'd be happy to. Which of the above solutions would you like to see included?
@douglas-treadwell
Hmm. I'll try to outline what I'm imagining:
is(...conditions) => action => booleantype property and is not a function), and the second argument is an array, they could have the signature you imagined aboveisAnyOf, isAllOfisAsyncThunkAction, isFulfilled, isRejected, isPending and maybe isRejectedWithValuecreateAsyncThunk to have { meta: { asyncThunkType: "pending"|"fulfilled"|"rejected"}} so that we don't just do a string match on the type property.all naming is open for discussion - I suck at naming things
So, usage examples:
builder.addMatcher(isAnyOf(actionA, actionB), () => { ... })
if (isAnyOf(action, [actionA, actionB])) { ... }
builder.addMatcher(isAsyncThunkAction(), () => { ... })
builder.addMatcher(isAsyncThunkAction(asyncThunkA, asyncThunkB), () => { ... })
if (isAsyncThunkAction(action)) { ... } // due to this signature, above could also be builder.addMatcher(isAsyncThunkAction, () => { ... }) - without the method invocation
if (isAsyncThunkAction(action, [asyncThunkA, asyncThunkB])) { ... }
@markerikson any further thoughts?
PS: maybe match instead of is could also be an idea. It would be more in line with current wording, but a little longer.
Seems reasonable to me in general.
@phryneas I'll have a PR for you shortly.
I opened a PR at https://github.com/reduxjs/redux-toolkit/pull/788. I'll be happy to make any improvements needed.
I started with core functionality only. After the initial PR review (and possibly the initial merge) I'll follow up with more functionality.
This would help with typing the useActions hook found here https://react-redux.js.org/api/hooks#recipe-useactions
import { bindActionCreators } from 'redux'
import { useDispatch } from 'react-redux'
import { useMemo } from 'react'
export function useActions(actions, deps) {
const dispatch = useDispatch()
return useMemo(
() => {
if (Array.isArray(actions)) {
return actions.map(a => bindActionCreators(a, dispatch))
}
return bindActionCreators(actions, dispatch)
},
deps ? [dispatch, ...deps] : [dispatch]
)
}
@alradadi I'm glad to hear it will help! I'm continuing to work on the related code today. Hopefully this will be a solved problem in the near future. It's been a busy week, but I'm trying to make time to help solve this issue.
@markerikson @phryneas I opened a Draft PR at https://github.com/reduxjs/redux-toolkit/pull/807.
I might be able to finish up the remaining functionality this week. If not, then the week after at the latest.
Most helpful comment
@alradadi I'm glad to hear it will help! I'm continuing to work on the related code today. Hopefully this will be a solved problem in the near future. It's been a busy week, but I'm trying to make time to help solve this issue.