2021-09-16
|~4 min read
|673 words
Last time I wrote about using React’s useReducer
with Typescript I ended up relying on Type Guards to get my actions typed and ended with an claim that I’d come back some day when I understood action creators a bit better.
Today’s that day! Well, kind of. While I do use Action Creators in this post, the primary focus was on how to get typing for my reducer to work as expected.
Building on the work I’ve done with HTTP requests, I wanted to see what a reducer would look like inside of a component.1
So, without further ado, let’s write a small reducer and call out some of the lessons learned:
import { Reducer, useReducer } from "react";
export enum RemoteDataState {
NotStarted = "Not Started",
Loading = "Loading",
Success = "Success",
Fail = "Fail"
}
export type RemoteData<T, E = string> =
| { state: RemoteDataState.NotStarted }
| { state: RemoteDataState.Loading }
| { state: RemoteDataState.Success; data: T }
| { state: RemoteDataState.Fail; message: E };
type AvailState = RemoteData<unknown>;
const initialAvailState: AvailState = {
state: RemoteDataState.NotStarted
};
enum AvailAction {
resetAvailability = "resetAvailability "
}
function resetAvailability() {
return {
type: AvailAction.resetAvailability
} as const;
}
type AvailActions = { type: AvailAction };
export default function App() {
const [avail, availDispatch] = useReducer<Reducer<AvailState, AvailActions>>(
(prevState, action) => {
switch (action.type) {
case AvailAction.resetAvailability: {
return { state: RemoteDataState.NotStarted };
}
default:
return prevState
}
},
initialAvailState
);
return (
/*...*/
);
}
The first time I wrote this, my reducer was just a plain function with typed arguments:
const [avail, availDispatch] = useReducer(
(prevState: AvailState, action: AvailAction) => {
switch (action.type) {
case AvailAction.resetAvailability: {
return { state: RemoteDataState.NotStarted }
}
default:
return prevState
}
},
initialAvailState,
)
Typescript, however, complained. Specifically:
No overload matches this call.
Overload 1 of 5, '(reducer: ReducerWithoutAction<any>, initializerArg: any, initializer?: undefined): [any, DispatchWithoutAction]', gave the following error.
Argument of type '(prevState: AvailState, action: AvailAction) => { state: RemoteDataState; } | undefined' is not assignable to parameter of type 'ReducerWithoutAction<any>'.
Overload 2 of 5, '(reducer: (prevState: RemoteData<unknown, string>, action: AvailAction) => { state: RemoteDataState; } | undefined, initialState: never, initializer?: undefined): [...]', gave the following error.
Argument of type 'RemoteData<unknown, string>' is not assignable to parameter of type 'never'.
Type '{ state: RemoteDataState.NotStarted; }' is not assignable to type 'never'.ts(2769)
Digging into the type file for React, my useReducer
was receiving this type:
function useReducer<R extends ReducerWithoutAction<any>, I>(
reducer: R,
initializerArg: I,
initializer: (arg: I) => ReducerStateWithoutAction<R>,
): [ReducerStateWithoutAction<R>, DispatchWithoutAction]
My reducer was getting typed as a ReducerWithoutAction
despite the fact that I was passing actions!
I was effectively getting:
type ReducerWithoutAction<S> = (prevState: S) => S
instead of:
type Reducer<S, A> = (prevState: S, action: A) => S
So, the simplest solution was just to type the return type on my reducer:
const [avail, availDispatch] = useReducer(
- (prevState: AvailState, action: AvailAction) => {
+ (prevState: AvailState, action: AvailAction): AvailState => {
switch (action.type) {
Once I did that, Typescript could understand that my returned object was in fact of type AvailState
and all was rosy.
But, useReducer
is a generic and it can receive types up front to simplify further. That’s why I ended with the final form above:
const [avail, availDispatch] =
useReducer<Reducer<AvailState, AvailActions>>(/*...*/)
The exercise was a good reminder of some of the limitations of Typescript, how to dig through Definitely Typed to get to an answer (a lesson I seem to be getting a lot lately), and how we can use Generic typings to aid in strong typing.
useReducer
like this which I currently have bundled in a thunk for post requests.Hi there and thanks for reading! My name's Stephen. I live in Chicago with my wife, Kate, and dog, Finn. Want more? See about and get in touch!