Search code examples
reactjstypescriptreact-hooksreact-typescriptreact-tsx

Custom Hooks with TypeScript - return value


I'm trying to experiment custom Hooks with TS (quite new on both). This useOption should take etiher a number or a boolean, and return the value with the same type, and the changer function. This because an option should be <input> with 'range' type, and one <input> as checkbox (but this could be extended with other input types). So I have an <Options /> component like this:

const Options = () => {
  const [pieces, setPieces] = useOption(10);
  const [speed, setSpeed] = useOption(5);
  const [sound, setSound] = useOption(false);

  return (
    <div className='Options'>
      <input
        type='range'
        value={pieces}
        onChange={setPieces}
        min='5'
        max='25' />
      <p>{pieces}</p>
      <input
        type='range'
        value={speed}
        onChange={setSpeed}
        min='1'
        max='10' />
      <p>{speed}</p>
      <input
        type='checkbox'
        value={sound}
        onChange={setSound} />
      <p>{sound ? 'Sound on' : 'Sound off'}</p>
      <button
        className='start-btn'
        onClick={handleClick}>Start game</button>
    </div>
  )
};

And a custom hook useOption.

const useOption = (initialValue: number | boolean) => {
  const [value, setValue] = useState(initialValue);

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    if (typeof(e.currentTarget.value) === 'boolean') {
      const newValue = !value;
      setValue(newValue);     
    } 
    else {
      const newValue = Number(e.currentTarget.value);
      setValue(newValue)
    }
  };

  return [value, handleChange] as const;
};

I thought, that with the typeof type guard and the use of const (like written in this page), TS recognized the returned type, but I still have errors about it, showing me the error over the value property.

Type 'number | boolean' is not assignable to type 'string | number | readonly string[] | undefined'.
  Type 'false' is not assignable to type 'string | number | readonly string[] | undefined'.ts(2322)
index.d.ts(2256, 9): The expected type comes from property 'value' which is declared here on type 'DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>'

Any suggestion about to solve it?


Solution

  • You can make the hook generic

    const useOption = <T extends number | boolean>(initialValue: T) => {
      const [value, setValue] = React.useState(initialValue);
    
      const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        if (typeof e.currentTarget.value === 'boolean') {
          const newValue = !value;
          setValue(newValue as T);
        } else {
          const newValue = Number(e.currentTarget.value);
          setValue(newValue as T);
        }
      };
    
      return [value, handleChange] as const;
    };
    

    And pass type in the component

    export default function App() {
      const [pieces, setPieces] = useOption<number>(10);
      const [speed, setSpeed] = useOption<number>(5);
      const [sound, setSound] = useOption<boolean>(false);
    
      return (
        <div className="Options">
          <input
            type="range"
            value={pieces}
            onChange={setPieces}
            min="5"
            max="25"
          />
          <p>{pieces}</p>
          <input type="range" value={speed} onChange={setSpeed} min="1" max="10" />
          <p>{speed}</p>
          <input type="checkbox" checked={sound} onChange={setSound} /> // use checked in here
          <p>{sound ? 'Sound on' : 'Sound off'}</p>
        </div>
      );
    }
    

    Demo