Search code examples
reactjstypescript

Optional action key causing error in useReducer of React in Typescript


In below, I am trying to use useReducer from react in Typescript. After I made one of the action object key as optional, I started getting this error. When it comes to the state, I no where mentioned any key as optional, but from the error message it appears TypeScript sees name can be optional. Not sure, what should be fixed. If I make nextName of action non-optional and pass empty string where I don't have anything to pass, It was working without this error.

Is this something you can help me understand?

import React from "react";
import logo from "./logo.svg";
import "./App.css";
import { useReducer } from "react";

function profileReducer(
  state: { name: string; age: number },
  action: { type: string; nextName?: string }
) {
  switch (action.type) {
    case "update_name": {
      return { name: action.nextName, age: state.age };
    }
    case "increment": {
      return { name: state.name, age: state.age + 1 };
    }
    case "decrement": {
      return { name: state.name, age: state.age - 1 };
    }
  }
  throw Error("Unknown Action !!!");
}

function App() {
  function handleTextChange(e: React.ChangeEvent<HTMLInputElement>) {
    dispatch({ type: "update_name", nextName: e.target.value });
  }

  function handleAgeChange(shouldIncrement: boolean) {
    shouldIncrement
      ? dispatch({ type: "increment" })
      : dispatch({ type: "decrement" });
  }
  const [state, dispatch] = useReducer(profileReducer, { name: "", age: 42 });
  return (
    <div className="App">
      <p />
      Name: <input type="text" value={state.name} onChange={handleTextChange} />
      <p />
      Age: {state.age}
      <p />
      <button onClick={() => handleAgeChange(true)}>+</button>
      <button onClick={() => handleAgeChange(false)}>-</button>
    </div>
  );
}

export default App;

Error on useReducer(profileReducer, { name: "", age: 42 }); saying profileReducer is not compatible.

Error Message:

 No overload matches this call.
  Overload 1 of 3, '(reducer: (prevState: { name: string; age: number; }, action: { type: string; nextName?: string | undefined; }) => { name: string; age: number; }, initialState: { name: string; age: number; }): [...]', gave the following error.
    Argument of type '(state: { name: string; age: number; }, action: { type: string; nextName?: string | undefined; }) => { name: string | undefined; age: number; }' is not assignable to parameter of type '(prevState: { name: string; age: number; }, action: { type: string; nextName?: string | undefined; }) => { name: string; age: number; }'.
      Call signature return types '{ name: string | undefined; age: number; }' and '{ name: string; age: number; }' are incompatible.
        The types of 'name' are incompatible between these types.
          Type 'string | undefined' is not assignable to type 'string'.
            Type 'undefined' is not assignable to type 'string'.
  Overload 2 of 3, '(reducer: (prevState: { name: string; age: number; }, action: { type: string; nextName?: string | undefined; }) => { name: string; age: number; }, initialState: { name: string; age: number; }): [...]', gave the following error.
    Argument of type '(state: { name: string; age: number; }, action: { type: string; nextName?: string | undefined; }) => { name: string | undefined; age: number; }' is not assignable to parameter of type '(prevState: { name: string; age: number; }, action: { type: string; nextName?: string | undefined; }) => { name: string; age: number; }'.
      Type '{ name: string | undefined; age: number; }' is not assignable to type '{ name: string; age: number; }'.

Solution

  • TS doesn't know which of your actions has nextName and which doesn't. You should explicitly type your actions, and use the name of the action as the type of the type property (the action type). Create a union of actions:

    type Action = { type: 'update_name'; nextName: string; } | 
                  { type: 'increment'; } | 
                  { type: 'decrement'; };
    

    In addition, in your reducer don't manually re-assign properties that you don't change, just spread the previous state, and override the properties that you do change.

    type Action = { type: 'update_name'; nextName: string; } | 
                  { type: 'increment'; } | 
                  { type: 'decrement'; };
    
    function profileReducer(
      state: { name: string; age: number },
      action: Action
    ) {
      switch (action.type) {
        case "update_name": {
          return { ...state, name: action.nextName };
        }
        case "increment": {
          return { ...state, age: state.age + 1 };
        }
        case "decrement": {
          return { ...state, age: state.age - 1 };
        }
      }
    }