Using Typescript generics in a callback invoking a React useState setter

According to Typescript, this code has the following problem: Argument of type 'number' is not assignable to parameter of type 'SetStateAction<T>'.ts(2345).

A similar version with a type or a type union in place of the generic would work (in fact a union with all the expected types would probably be recommended).

Still, if I wanted to stick with generics how would I modify this code so that it is accepted?

import React, { Dispatch, SetStateAction, useState } from 'react';

function useValue<T>(init: T) {
  return useState<T>(init);
}

function SpecialButton<T>({
  value,
  setValue,
}: {
  value: T;
  setValue: Dispatch<SetStateAction<T>>;
}) {
  switch (typeof value) {
    case 'number':
      return <button onClick={() => setValue(value + 1)}>Add 1 to {value}</button>;
    default:
      return <div>UNSUPPORTED</div>;
  }
}

function App() {
  const [value, setValue] = useValue<number>(0);
  return <SpecialButton value={value} setValue={setValue} />;
}

(Also, I noticed that in the case of type equal to number value would be cast to T | number, but I'm not sure how to take advantage of that).


Solution 1:

I can suggest you the next approach of narrowing types:

function useValue<T>(init: T) {
  return useState<T>(init);
}

type Parameters<T> = {
  value: T;
  setValue: Dispatch<SetStateAction<T>>;
};

const isNumberParams = (parameters: { value: unknown }): parameters is Parameters<number> =>
  typeof parameters.value === 'number';

const isStringParams = (parameters: { value: unknown }): parameters is Parameters<string> =>
  typeof parameters.value === 'string';

function SpecialButton<T>(params: Parameters<T>) {
  if (isNumberParams(params)) {
    return (
      <button onClick={() => params.setValue(params.value + 1)}>Add 1 to {params.value}</button>
    );
  }
  if (isStringParams(params)) {
    return (
      <button onClick={() => params.setValue(params.value + '1')}>Add 1 to {params.value}</button>
    );
  }
  return <div>UNSUPPORTED</div>;
}

function App() {
  const [value, setValue] = useValue<number>(0);
  return <SpecialButton value={value} setValue={setValue} />;
}