Converting a React component to TypeScript
Tags: javascript, react, typescript, webdev
Published: 2021-10-08 07:36:02

In this post you will join me as I modify a simple component to start utilizing TypeScript.
The [WordSearch game](https://word-search-react-game.netlify.app/) 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](https://create-react-app.dev/docs/adding-typescript/) 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q6e7a8wwlszauls5gwd6.png)
 
Here is the original code which will help you follow through:

```javascript
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;
```

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}) => {
     |                   ^
```

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](https://kentcdodds.com/blog/how-to-write-a-react-component-in-typescript#:~:text=With%20that%2C%20let%27s%20try%20some%20options%20for%20applying%20that%20type%20to%20the%20props%20object%20in%20our%20React%20Component.), and also the caveats of using React.FC in this [detailed StackOverflow answer](https://stackoverflow.com/a/59991281), 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:

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

And the usage looks like this:

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

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);
      |    
```

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:

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

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:

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

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:

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

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;
```

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

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

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.

```javascript
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;
```

Cheers :)

*Hey! If you liked what you've just read be sure to also visit me on twitter* :) **<a href="https://twitter.com/mattibarzeev?ref_src=twsrc%5Etfw" class="twitter-follow-button" data-show-count="false">Follow @mattibarzeev</a><script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>** :beers: