Converting a React component to TypeScript

Matti Bar-Zeev - Oct 8 '21 - - Dev Community

In this post you will join me as I modify a simple component to start utilizing TypeScript.
The WordSearch game which I’m experimenting on was built using CreateReactApp so I will follow their guide on how to enable TS on an existing project.

First we need to install the packages which enable typescript on a project

  • Typescript - the package which enables the actual TS compiler
  • @types/node - the package which contains type definitions for Nodejs
  • @types/react - the package which contains type definitions for React
  • @types/react-dom - the package which contains type definitions for React DOM
  • @types/jest - the package which contains type definitions for Jest

The docs from CreateReactApp tell me to install these as runtime deps, but I think that their place is under the dev deps, so this is where I will install them :)

I’m going to take the AddWord component and convert it to use TS. This component is responsible for adding a new word to the in the words panel for the WordSearch game.

image

Here is the original code which will help you follow through:



import React, {Fragment, useEffect, useRef, useState} from 'react';
import Add from '@material-ui/icons/Add';

const AddWord = ({onWordAdd}) => {
   const inputEl = useRef(null);
   const [newWord, setNewWord] = useState('');
   const [disable, setDisable] = useState(true);

   useEffect(() => {
       // A word is valid if it has more than a single char and has no spaces
       const isInvalidWord = newWord.length < 2 || /\s/.test(newWord);
       setDisable(isInvalidWord);
   }, [newWord]);

   function onAddClicked() {
       onWordAdd && onWordAdd(inputEl.current.value);
       setNewWord('');
   }

   function onChange(e) {
       const value = e.target.value;
       setNewWord(value);
   }

   return (
       <Fragment>
           <input
               type="text"
               name="new"
               required
               pattern="[Bb]anana|[Cc]herry"
               ref={inputEl}
               placeholder="Add word..."
               value={newWord}
               onChange={onChange}
           />
           <button onClick={onAddClicked} disabled={disable}>
               <Add></Add>
           </button>
       </Fragment>
   );
};

export default AddWord;


Enter fullscreen mode Exit fullscreen mode

I start by changing the file extension to .tsx - src/components/AddWord.js > src/components/AddWord.tsx

Launching the app I’m getting my first type error:



TypeScript error in 
word-search-react-game/src/components/AddWord.tsx(4,19):
Binding element 'onWordAdd' implicitly has an 'any' type.  TS7031

   2 | import Add from '@material-ui/icons/Add';
   3 |
 > 4 | const AddWord = ({onWordAdd}) => {
     |                   ^


Enter fullscreen mode Exit fullscreen mode

Let’s fix that.
The problem here is that the component does not declare the type of props it allows to be received. I saw 2 methods of addressing this issue. One is using the React.FC and the other is approaching this function component as a function and therefore regard its typing as a function without React’s dedicated typings. Reading Kent C. Dodds' article about the issue, and also the caveats of using React.FC in this detailed StackOverflow answer, I decided to go with the conventional function typing way.
Ok, so we need to define the props type. I would like to go with Interface instead of a type, coming from an OOP background, I know that working against interfaces is by far much more flexible.
There is a single prop this component receives and it is a callback function, which has a string argument and returns nothing (I like to mark my interfaces with an “I” prefix).
Our props interface looks like this:



interface IAddWordProps {
   onWordAdd: (value: string) => void;
}


Enter fullscreen mode Exit fullscreen mode

And the usage looks like this:



const AddWord = ({onWordAdd}: IAddWordProps) => {
...


Enter fullscreen mode Exit fullscreen mode

That solved that, on to the next error:



TypeScript error in 
word-search-react-game/src/components/AddWord.tsx(20,32):
Object is possibly 'null'.  TS2531

   18 |
   19 |     function onAddClicked() {
 > 20 |         onWordAdd && onWordAdd(inputEl.current.value);
      |    


Enter fullscreen mode Exit fullscreen mode

Which is true, the inputEl can potentially be null, so how do we go about it?
In general, I don't like suppressing errors and warnings. If you decide to use a tool you don’t need to be easy on the “disable rule” configuration of it, so let’s try and really solve this one.
First I would like to set a type to the inputEl ref, and it can be either null or a React.RefObject interface which has a generics type to it. Since we’re dealing with an input element, it would be HTMLInputElement. The inputEl typing looks like this now:



const inputEl: RefObject<HTMLInputElement> | null = useRef(null);


Enter fullscreen mode Exit fullscreen mode

Still, this does not solve our main issue. Let’s continue.
One option to solve this issue is using optional-chaining, which means that we know and prepare our code to gracefully handle null pointers. The handler looks like this now:



function onAddClicked() {
   onWordAdd && onWordAdd(inputEl?.current?.value);


Enter fullscreen mode Exit fullscreen mode

But once we do that we have broken the interface of the props we defined earlier, since it expects to receive a string and now it can also receive undefined, so let’s fix the interface to support that as well:



interface IAddWordProps {
    onWordAdd: (value: string | undefined) => void;
}


Enter fullscreen mode Exit fullscreen mode

Done. On to the next error.



TypeScript error in 
word-search-react-game/src/components/AddWord.tsx(24,23):
Parameter 'e' implicitly has an 'any' type.  TS7006

   22 |     }
   23 |
 > 24 |     function onChange(e) {
      |                       ^
   25 |         const value = e.target.value;


Enter fullscreen mode Exit fullscreen mode

The solution here is simple - I’m adding the ChangeEvent type to e. Now it looks like this:



function onChange(e: ChangeEvent<HTMLInputElement>) {
    const value = e.target.value;
    setNewWord(value);
}


Enter fullscreen mode Exit fullscreen mode

This is not a “React type” and as for now I don’t see any reason to use React types when not needed (if you do know of such a reason, please share in the comments).

And that’s it! The application is back and running :)
Below you can find the modified code (with some additional, non-critical types added) and you can compare it to the original one at the start of this post.

update -
After some great feedback in the comments below (and on Reddit) I've made some modifications in the code accordingly. Thanks guys.



import React, {ChangeEventHandler, MouseEventHandler, RefObject, useRef, useState} from 'react';
import Add from '@material-ui/icons/Add';

interface IAddWordProps {
    onWordAdd?: (value: string | undefined) => void;
}

const AddWord = ({onWordAdd}: IAddWordProps) => {
    const inputEl: RefObject<HTMLInputElement> | null = useRef(null);
    const [newWord, setNewWord] = useState('');
    const [disable, setDisable] = useState(true);

    const onAddClicked: MouseEventHandler<HTMLButtonElement> = () => {
        onWordAdd?.(newWord);
        setNewWord('');
    };

    const onChange: ChangeEventHandler<HTMLInputElement> = ({currentTarget: {value}}) => {
        setNewWord(value);
        // A word is valid if it has more than a single char and has no spaces
        const isInvalidWord: boolean = value.length < 2 || /\s/.test(value);
        setDisable(isInvalidWord);
    };

    return (
        <>
            <input
                type="text"
                name="new"
                required
                pattern="[Bb]anana|[Cc]herry"
                ref={inputEl}
                placeholder="Add word..."
                value={newWord}
                onChange={onChange}
            />
            <button onClick={onAddClicked} disabled={disable}>
                <Add />
            </button>
        </>
    );
};

export default AddWord;


Enter fullscreen mode Exit fullscreen mode

Cheers :)

Hey! If you liked what you've just read be sure to also visit me on twitter :) Follow @mattibarzeev 🍻

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .