Reputation: 2448
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.
Upvotes: 12
Views: 11312
Reputation: 2448
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.
Upvotes: 20
Reputation: 23463
You need to give the specific reducer case some more type info by type-casting the action
type UserActionWithPayload = {
type: string;
username: string;
};
type UserActionWithoutPayload = {
type: string;
};
type UserAction = UserActionWithPayload | UserActionWithoutPayload;
function userReducer(state: string, action: UserAction): string {
switch (action.type) {
case "LOGIN":
return (action as UserActionWithPayload).username;
case "LOGOUT":
return "";
default:
throw new Error("Unknown 'user' action");
}
}
let state = "";
state = userReducer(state, { type: "LOGIN", username: "John" });
console.log("LOGIN", state);
state = userReducer(state, { type: "LOGOUT" });
console.log("LOGOUT", state);
Upvotes: -1
Reputation: 9787
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");
}
}
Upvotes: 1
Reputation: 182
function userReducer(state: string, action: UserAction): any {
switch (action.type) {
case "LOGIN":
return action.username;
case "LOGOUT":
return "";
default:
throw new Error("Unknown 'user' action");
}
}
In this function, you defined return type as a string. But when you pass down UserAction as UserActionWithoutPayload type, the return value should be undefined. These causes differ between your userReducer(...) function return type and real return type as a string and undefined. To fix this, you can change return type as any.
I hope this answer might be helpful in your work.
Regards. Lin.
Upvotes: 1