0

Newish to TS. I have a very simple demo app that uses the useReducer() hook to manage the selected state of items in a list. I thought I did a good job of making the reducer type safe but when I call the useReducer() hook I get the TS2769: No overload matches this call error. I will list the contents of the reducer file, the component which calls useReducer() and then the exact error text, and a sandbox link:

Here is my reducer file:

// src/Components/List/Reducer.ts
export type ListAction = {
    type: 'TOGGLE_SELECTED'
    index: number
}

export type ListItemState = {
    name: string
    color: string
    selected: boolean
}

const reducer = (state: ListItemState[], action: ListAction): ListItemState[] => {
    const {index, type} = action
    switch(type) {
        case 'TOGGLE_SELECTED':
            const item = state[index]
            const newState = state.slice()
            newState[index] = {...item, selected: !item.selected}
            return newState
        default:
            return state
    }
}

export default reducer

Here is the component which calls useReducer():

//src/Components/List/List.tsx
import React, {useReducer} from "react";
import Reducer, {ListItemState} from "./Reducer";
import InitialState from "./InitialState";
import ListItem from "../ListItem/ListItem";
import SelectedItems from "../SelectedItems/SelectedItems";
import "./List.css";

const List = () => {
    const [state, dispatch] = useReducer(Reducer, InitialState);
    const selected = state
        .filter((item: ListItemState) => item.selected)
        .map((selected: ListItemState) => selected.name);
    return (
        <>
            <SelectedItems items={selected} />
            <ul className="List">
                {state.map((item: ListItemState, index: number) => (
                    <ListItem {...item} key={item.name} index={index} dispatch={dispatch} />
                ))}
            </ul>
        </>
    );
};

export default List

Here is the exact error text:

Compiled with problems:X

ERROR in src/Components/List/List.tsx:10:31

TS2769: No overload matches this call. Overload 1 of 5, '(reducer: ReducerWithoutAction, initializerArg: any, initializer?: undefined): [any, DispatchWithoutAction]', gave the following error. Argument of type '(state: ListItemState[], action: ListAction) => ListItemState[]' is not assignable to parameter of type 'ReducerWithoutAction'. Overload 2 of 5, '(reducer: (state: ListItemState[], action: ListAction) => ListItemState[], initialState: ListItemState[], initializer?: undefined): [...]', gave the following error. Argument of type 'string[]' is not assignable to parameter of type 'ListItemState[]'. Type 'string' is not assignable to type 'ListItemState'. 8 | 9 | const List = () => {

10 | const [state, dispatch] = useReducer(Reducer, InitialState); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 11 | const selected = state 12 | .filter((item: ListItemState) => item.selected) 13 | .map((selected: ListItemState) => selected.name);

Neil Girardi
  • 4,533
  • 1
  • 28
  • 45
  • Your initial state is `string[]`, but your reducer expects `ListItemState[]`. – AKX Jul 15 '22 at 12:17
  • As an aside, the Reducer.tsx file in your sandbox is empty. – AKX Jul 15 '22 at 12:22
  • Thanks @AKX. The initial state is an array of objects like this: `[{name: 'foo', color: 'red' selected: false}]`. – Neil Girardi Jul 15 '22 at 12:27
  • 1
    Well, no, it's not in your example ;-) – AKX Jul 15 '22 at 12:28
  • @AKX The initial state is the `items` exported from `src/Components/List/InitialState`. If I console that variable it is an array of objects. I understand that TS _thinks_ it's an array of strings but it doesn't appear so. What am I missing? Thank you. Updated link: https://codesandbox.io/s/romantic-ride-j0y9y4 – Neil Girardi Jul 15 '22 at 12:34
  • It's that `reduce` pyramid that's not properly typed – when in doubt, don't use `reduce`, TS or not... – AKX Jul 15 '22 at 12:40

2 Answers2

1

Here's a type-safe simplification of your code in a single file. The reduce pyramid you had in your InitialState file is throwing TS off.

In other words, your reducer and state are fine, it's just that your initial data indeed wasn't soundly typed.

Sandbox here.

import React, { useReducer } from "react";

function getInitialState(): ListItemState[] {
  const sizes = ["tiny", "small", "medium", "large", "huge"];
  const colors = ["blue", "green", "orange", "red", "purple"];
  const fruits = ["apple", "banana", "watermelon"];
  const items = [];
  for (const size of sizes) {
    for (const color of colors) {
      for (const fruit of fruits) {
        items.push({
          name: `${size} ${color} ${fruit}`,
          color,
          selected: false
        });
      }
    }
  }
  return items;
}
type ListAction = {
  type: "TOGGLE_SELECTED";
  index: number;
};

type ListItemState = {
  name: string;
  color: string;
  selected: boolean;
};

function reducer(state: ListItemState[], action: ListAction): ListItemState[] {
  const { index, type } = action;
  switch (type) {
    case "TOGGLE_SELECTED":
      const item = state[index];
      const newState = [...state];
      newState[index] = { ...item, selected: !item.selected };
      return newState;
    default:
      return state;
  }
}

function List() {
  const [state, dispatch] = useReducer(reducer, null, getInitialState);
  const selected = state
    .filter(({ selected }) => selected)
    .map(({ name }) => name);
  return (
    <>
      {JSON.stringify(selected)}
      <div>
        {state.map(({ name, color }, index) => (
          <button
            key={name}
            onClick={() => dispatch({ type: "TOGGLE_SELECTED", index })}
            style={{ color }}
          >
            {name}
          </button>
        ))}
      </div>
    </>
  );
}

export default function App() {
  return (
    <div className="App">
      <List />
    </div>
  );
}
AKX
  • 152,115
  • 15
  • 115
  • 172
  • AKX You are a gentleman and a scholar. Thank you! – Neil Girardi Jul 15 '22 at 12:47
  • @NeilGirardi Glad I could help! As an aside, whenever you find yourself typing the dreaded trigram `any` in TypeScript, lean back and take a deep breath and reconsider your life choices, then rewrite what you're doing without `any`. 99% of the time, `any` is a code _stench_. – AKX Jul 15 '22 at 12:49
  • indeed! I recently had a similar TS issue and that code also used Array.prototype.reduce. Is there a trick to using reduce with TS? Does one generally have to avoid reduce when using TS? – Neil Girardi Jul 15 '22 at 12:51
  • Well, I'd say generally avoid `reduce`, in both TS and JS. It usually leads to unreadable and complex code. – AKX Jul 15 '22 at 13:53
0

After reading @akx answer and trying his excellent solution I decided to go back and attempt the original reduce "pyramid" solution. It turned out to be fairly straight forward. After converting the items variable to a function I had make the return type of that function ListItemState[], and I also had to make ListItemState[] the type of the accumulator (ie previous) value of both of the Array.prototype.reduce() callbacks:

const getInitialState = (): ListItemState[] => sizes.reduce(
    (items: ListItemState[], size: string) => [
        ...items,
        ...fruits.reduce(
            (acc: ListItemState[], fruit: string) => [
                ...acc,
                ...colors.reduce(
                    (acc: any[], color: string) => [
                        ...acc,
                        {
                            name: `${size} ${color} ${fruit}`,
                            color
                        }
                    ],
                    []
                )
            ],
            []
        )
    ],
    []
);
Neil Girardi
  • 4,533
  • 1
  • 28
  • 45
  • 1
    This is also a lot less efficient, mind, since you're taking tons and tons of array copies. Sometimes a for loop is better... – AKX Jul 15 '22 at 15:08
  • I'm not sure why you think this is "fairly straight forward"... It's 10 times easier to look at the loop to understand what's going on and as @AKX mentioned, it's less efficient. But yes, you can do it using nested reduces, just don't hope that others will be able to grok it. – Ruan Mendes Jul 15 '22 at 15:13
  • @JuanMendes duly noted. I wanted to get this to work with the reduce calls because I recently ran into a similar issue doing a JS -> TS conversion on a file with some calls to `Array.prototype.reduce()`. Therefore, I wanted to figure out how to make `.reduce()` work with TS. I think what I learned it that it may not be enough to type the return value of the function calling `.reduce`. You may need to type the `.reduce()` callback. And if you pass an empty array or an empty object as the optional initial `.reduce()` values, you may need to type those as well. – Neil Girardi Jul 16 '22 at 13:53
  • It is pretty cool I just disagreed with the "fairly straightforward" – Ruan Mendes Jul 17 '22 at 13:48