Adding types to native React Hooks. All about including TypeScript in a React Project.
This post will show you how to include TypeScript in an already started project. Of course, I always recommend using it from the start as it provides many great developer features.
As seen in previous examples of plain Redux and hooks, there are many appearances of data that are not properly checked before it’s rendered. You can still trace those issues if you have a good and configured IDE which may prompt you about a possible bug. Anyway, we are allowed to render e.g. image like that:
By now, you can probably tell what I mean. We are not sure if the
REACT_APP_API_KEY property exists in
env variables, so we can face an error from JS and break the app if it’s missing. We have to define in our code if this value is required (so we can safely expect it anytime), or if it’s optional (and what to do in case it’s absent).
To get started, you have to install
typescript package along with types supporting packages for some other external libraries like e.g.
@types/react to use with React, or… you can use CRA as before. As you remember, we’re going to do the first command to install Create React App the same way, but with one additional parameter:
npx create-react-app my-app --template typescript
This will cover and manage for you all required TypeScript functionality, dependencies, and configuration which you can always modify for your own purposes via
Now you can start writing your app, but in our case, we will just copy
src folder from the last hooks implementation. You can choose your own way and for better experience start writing it from scratch.
If your app is already built on PropTypes, you would set those properties as required or make them default valued, and that can keep you informed in devtools if you are "doing a good job". Using PropTypes you may be interested in inferring your types using
TS has also a great feature hidden under CTRL + SPACE shortcut for code completion based on our types. Imagine we created types for
team object and then after calling
team. We can use a shortcut for all possible values we may expect:
TypeScript is always recommended to be written simultaneously with the rest of the code to better control your application. Always consider using TS when you are starting a project. It’s often difficult to convert the existing code and you may face some code restructuring.
The biggest challenge is to find a proper place to include your types or interfaces. As you switch your files to
.ts extension and place them in a TS-supported environment, you will face many errors. When trying to provide types to specific files it can turn out that your types should be somewhere higher in the project tree to avoid redundant types in other places. That’s why it’s always good to start from the highest to lowest file in your components hierarchy.
In our case, it’s best to start from our store, as it is wrapping the whole app and providing it with some data.
Let's switch all our
*.jsx files into
.ts files. It depends on the file. The
.tsx format can be used to parse
JSX content type, so it’s used for React code. Actually, it’s a requirement to use TSX when writing JSX code. If you don’t use TypeScript, you can keep your JSX React code in plain
.js files. Plain JS functionalities like our services should use
TypeScript for React context and useReducer hook
Now, let’s go to our index file of store folder and make following changes:
Instead of creating context to store our global state values, we can define expected appearance of this data. In our case, we called it
Please also note the use of
Partial<> element of TypeScript. It allows us to use an empty initial value when the app is rendered for the first time when we didn't provide any data to context yet. In that case, you will have to use a conditional for every context value used around the app to try to set some default ones to be sure they will always be consumed by components.
To handle that, remove
Partial and add some initial state:
Default values as better user experience
This will make TS yelling about dispatch usage (as it doesn't exist in value of initial create context), so we have to make sure we trigger it only when available:
Or whichever method you prefer. By providing an initial value (example below), we can omit these conditions (so the first example will not cause any errors like before) and in case of any problems display some basic alert to our user that something was wrong, but nothing lost yet and still can retry 😉
It will highly improve our application UX:
state elements can be left empty as we need it to reflect the component statuses, like displaying loadings, errors, etc. We also want to export this value to use in reducers initial states later. For the dispatch function without any default value, you will be forced to trigger it conditionally, like in the above examples. As we mentioned, TypeScript is checking functionality at compile-time. While it is undefined at first render, it will stop us from doing so unless we use some conditional to be sure it exists and we can safely trigger it.
We know we are passing two values to make our component able to communicate and contribute to our store. Those values are
state with app data, and
dispatch function to fire assigned actions. Dispatch as you may have noticed is defined as a function returning nothing (
void) but taking an
Action type as a parameter. This parameter is an object of action type and some payload depends on which one we are dispatching. So for now, let’s create a type that can be one of our actions types, but we will talk about later:
...and add type for the second context param as follows:
In store state, we are collecting two values as above, so we need also types for them.
Current will be just a number corresponding to a chosen team ID, and
teams suppose to be an array of received teams from an API. We can type them to look like this.
You can use a smart tip to handle them. To remind, you we are using football-data.org, but whatever service you use, you can take a look at the API structure and get values you need to render components. There are also great online converters for TypeScript, so you can make a test request to your API endpoint, copy all JSON responses and paste it to one of them, eg. https://app.quicktype.io. Then, you can take retrieved Types into your code and adjust if needed.
In TypeTeam there is one custom and optional key called stats. This is where we are collecting statistics for a current ‘team’ after it was clicked for later use. In the first save of the ‘team’ object, this is hasn’t occurred yet, that's why it's set as optional. So we can actually define it as well somewhere else:
It's good to export them already as we expect to use them later in components.
Typing through store
Now that we have all context and store values handled, we only need to add types to store Provider itself. We’ll use the same initial value for useReducer hook as for context. For rect functional components it's most common to use
React.FC<> which takes an component props types:
ReactNode is an element that suits best to all children that we want to pass via the children prop. You can play with other possible ones as there are some, but they are too specific and cannot handle all our purposes.
Now lets go a bit deeper into our actions and reducers. First of all we should type our static variables and the only ones we have are the actions names saved to constants. It's a common Redux practice, but as for now, we have to use them not only as a
type property of a reduced action, but also in TypeScript to check the current
We could define just a
string type for
type property and it would be true, but then we allow other developers to trigger actions that are not registered. To communicate our code and local environment what kind of actions we are able to use, we can use
enum and all other Typescript features, you can read in their documentation, but as a TL;DR, I can quote the most important part:
Enums allow us to define a set of named constants. Here is how they look in our application:
Now, we can use their values as normal variables. Treat it like plain JS object :
Continuing typescripting actions we have to define what type of prop it receives and what type it returns:
So we need to define a type for this
action. This type is one of our
Action types which is a param for the reducer function (and as you can see it's just a plain object with the action of
Actions.ADD_TEAMS and an array of before typed team
TypeTeam). As a reminder:
With that, we can be 100% sure
addTeams will never be used incorrectly or with wrong data values as your project will just stop you or any other developer who wants to use it.
Accordingly, we can deliver other types like
setCurrentTeam functions (
Then let's go to reducers and make sure they are using proper action types when triggered. To do that, we have to switch all
case statements from before plain constants to new
enum object so:
Just make sure you imported proper
Actions object from where you created that. I put it into the main store index file as it is sharable content through all
This is a place where the aforementioned glitch (of switching app to TypeScript instead of providing it from with project from start) is perfectly visible. When we open the
reducers.ts file to make it at least renderable, first we need to switch the actions types as they are declared differently now. But when you start writing your code with Typescript from scratch, you can enjoy all its features like this one:
This is our reducer definition with types. As every reducer, it takes state and action objects and returns a new state. So in the reducer responsible for
teams part of the application, we will have a state with initial data as imported from the context default state. As we are passing here a specific value, the Typescript will use its type as the variable we are assigning to. Your IDE should show you this information:
Then, as a second
action parameter, we use also an already defined
Action type which resolves to one of our action registered. The only thing you should remember is to keep up to date the types describing them whenever you add a new one:
End then we can tell TypeScript that this reducer will return an array of
TypeTeam objects which are part of our store along with
current from our second reducer which is actually desired case.
Whenever you start typing from that side, then you can really fast and easy build your reducer statements:
Just pick your desired one, hit enter, and finish with your custom functionality.
Now we can do the same for all
current actions and reducers (which is literally one :D), but I would omit that to not extend the content of this article unnecessarily. You can always see the whole project at the end of the post in the codesandbox.io environment.
How React components react to typed data
We have finished typing all our global store functionality from simple actions through the whole
useReducer. Time to use them in components and make them properly typed as well.
Let's start with the first component which uses store which is
Teams. You and your co-developers do not need to look for possible values at various files anymore. You can see them live while typing as well as expected types for them and quickly use to save some development time:
Teams component itself does not provide any props any so we don't have any Types there to register.
We have to just adjust the code a little to make it work properly with our store data, but your IDE (with a little help of eslint which I strongly recommend using to improve good practices and formats for your code) should help you with it easily trace the breaking parts.
First of all, we have to change conditionals for
teams property as it is an empty array at its initial state now. We've been quite forced to do it, to keep our data flow safer and more consistent. So now we need to check if the property is not empty, instead if it just exists as some value, and we have to do it for every place we checked the
and in loop rendering single Team:
Please also note the
?. appearance, as an optional chaining operator. Typescript also wants us to be sure that
team variable exists before we ask for its length, so this operator can help us to determine it.
As an addition and to keep all thing consistent, we can add types to each of our components functionalities, so for a simple state like here:
We can precise that state has to be a string. And for this purpose, I think is best to do the same as we make for store actions to limit possibilities and be sure that we have only expected values. So you can define another type for them or just hardcode like this:
We won't define Types for
combineReducers() function or
Loading component, as they are just middleware helpers, but you can try it on your own as a good exercise for learning TypeScript. We can just enforce some proper data flying through our
apiConnection() helper service:
All you have to do after switching the file to
.ts extension is to make sure an endpoint is passed as
But if you are perceptive, you may have already noticed that types auto-completion from TypeScript will add
string type automatically if we assign a string typed value as a default initial one, so this part is not required.
Then, we can make sure our function will return a
Promise which from the above code is representing the Type element. As a Promise value we will use
any because the server responses are way more complicated than we use, and we actually do not need to care about all of them, so we can keep focus only on if it is
Promise to make our element properly consume data providing by it (we will be sure in there that we have a Promise under that variable and can use
.then() for example to read it) and make other types checking depends on which endpoint they triggered.
Then TypeScript forces us to make sure that variable
process.env.REACT_APP_API_KEY exists before we can use it. So to prevent further problems in case of missing key, we can use a simple conditional and throw an error whenever it happens, while the application couldn't work without it.
Please have in mind that you’d like to show it to the users with the best UX possible 🙂
This should be fairly enough to cover the API connection functionality as we are the most interested in the data it takes and returns. Since we have all store functionality, API connection, and Teams component accurately typed, we can finish the rest by doing quite the same:
The Team only consume data, so to be sure we passing correct data let’s just type our prop to be consistent:
For Statistics it would be good to provide our local state types:
The initial state for
null as it waits for user clicks one of the teams, so we can use OR operator from TS to load here just
TypeTeam registered before.
You may have noticed the same type for
status state as we had on
Teams component. In case several components use similar or event the same states is good to extract Types to external file. Of course, at first, you have to consider extracting functionality itself, as it may be a good place to use a custom hook and avoid redundant code. One of the good practices is to create
types.ts file in the root of your project with all shared types.
If your code follows mine, I can bet you already know what you have to do differently for
Statistics. If not, just compile your code and let TypeScript tell you what you have to care about to make your component and code safe 😉
Anyway, we have to do the same thing as for
Teams and make sure we are properly reading the
teams variable like eg. in the first effect:
or down there in rendered component:
TypeScript is restricted in every place you defined it to be, so it will also tell you to make sure if
currentTeam exists before you will ask for its
name property, because as you remember we defined it to be null at a default state. I can recommend one of my favorite and the fastest ways to do that checking as I’m using it everywhere:
In most cases, it would be better to include the
currentTeam check in the first conditional to prevent render the entire Loading. In this case thanks to our code structure we are sure that
loading status is set only when
Please do not forget you can always use, play and test at example environments as promised I created for you on codesandbox, or clone the repo and play with them at your local:
BONUS: As we discuss code and variables safety it is also good to remember to prevent context usage out of Provider. It's a developer issue but whenever you work in team, it would be much helpful to provide some checker whether you trying to use it in place you can do so (inside Provider) or not.
To handle this case I would recommend you to create your own custom hook to trigger context imports and place it in services folder:
Now, you have to replace every occurrence of your
useContext(AppContext)] usage to just
useContextData() and you can be sure that context has no way to be used outside of its
Provider and as an additional advantage you will not have to import
AppContext anymore in each of those components while
useContextData will be enough and will do all things for you.
TypeScript leverage makes our properties more discoverable and consistent. It's not restricted to any specific area, like hooks or functions. You can add it to any JS statement and take advantage of typing plain variables etc. However, t's not the only advantage of using TS. Thanks to great code completion supporting you can save plenty of time since you started your project already with TypeScript supported.