Let's say we have userReducer
defined like this:
function userReducer(state: string, action: UserAction): string {
switch (action.type) {
case "LOGIN":
return action.username;
case "LOGOUT":
return "";
default:
throw new Error("Unknown 'user' action");
}
}
What's the best way to define UserAction
type so it will be possible to call dispatch
both with username
payload and without:
dispatch({ type: "LOGIN", username: "Joe"}});
/* ... */
dispatch({ type: "LOGOUT" });
If type is defined like this:
type UserActionWithPayload = {
type: string;
username: string;
};
type UserActionWithoutPayload = {
type: string;
};
export type UserAction = UserActionWithPayload | UserActionWithoutPayload;
Compiler throws and error in reducer in the "LOGIN" case: TS2339: Property 'username' does not exist on type 'UserAction'. Property 'username' does not exist on type 'UserActionWithoutPayload'.
If type is defined with optional member:
export type UserAction = {
type: string;
username?: string;
}
Then compiler shows this error: TS2322: Type 'string | undefined' is not assignable to type 'string'. Type 'undefined' is not assignable to type 'string'.
What's missing here? Maybe the whole approach is wrong?
Project uses TypeScript 3.8.3 and React.js 16.13.0.
The useReducer hook accepts a reducer type (state, action) => newState and returns a state object paired with a dispatch method much like Redux. Now the official useReducer documentation will show you how to define a reducer that accepts actions you will call with a dispatch method.
TypeScript lets you type-check your code in order to make it more robust and understandable. In this guide, I will show you how to set up TypeScript types on React hooks (useState, useContext, useCallback, and so on). Let's dive in.
In addition, TypeScript ensures that the initial state (the second parameter of useReducer) is a full State object. Finally, when we call dispatch, it must be a valid Action object. There’s no need to use a constant for the 'add_todo' type because it must match one of the valid Action types.
So it knows state is State and that dispatch is a function that can only dispatch Action objects. In addition, TypeScript ensures that the initial state (the second parameter of useReducer) is a full State object. Finally, when we call dispatch, it must be a valid Action object.
After hours of digging and experimenting found quite an elegant solution via Typescript enum
and union types for actions:
enum UserActionType {
LOGIN = "LOGIN",
LOGOUT = "LOGOUT"
}
type UserState = string;
type UserAction =
| { type: UserActionType.LOGIN; username: string }
| { type: UserActionType.LOGOUT }
function userReducer(state: UserState, action: UserAction): string {
switch (action.type) {
case UserActionType.LOGIN:
return action.username;
case UserActionType.LOGOUT:
return "";
default:
throw new Error();
}
}
function App() {
const [user, userDispatch] = useReducer(userReducer, "");
function handleLogin() {
userDispatch({ type: UserActionType.LOGIN, username: "Joe" });
}
function handleLogout() {
userDispatch({ type: UserActionType.LOGOUT });
}
/* ... */
}
No errors or warnings using approach above, plus there is a quite strict contract for action usage.
The approach looks ok, the problem is that your reducer has a return type of string
but if it is passed a UserActionWithoutPayload
then it might return action.username
where username is undefined.
So one way to fix it would be to relax your return types:
function userReducer(state: string, action: UserAction): string | undefined {
switch (action.type) {
case "LOGIN":
return action.username;
case "LOGOUT":
return "";
default:
throw new Error("Unknown 'user' action");
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With