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.
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;
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, 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;
}
And the usage looks like this:
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:
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:
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:
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 type to e. Now it looks like this:
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.
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 :) Follow @mattibarzeev 🍻