31
Quick start with Typescript and React
Currently, the React+Typescript
is considered as one of the most popular bundles for creating client-side applications. This combination has a huge potential and allows to develop solutions of any complexity. In this article, we’ll see why Typescript
is so hyped, discuss some basic examples of working with components, storages, and API queries, and see the benefits of Typescript
.
Table of contents
- Introduction
- React
- Typescript
- Project creation
- Components
- Store
- API
- Props of Typescript
- Const of Typescript
- Conclusion
I assume that all readers know about React
. Otherwise, it is pointless to proceed reading the article. To enthusiastic non-reacters please go to read the doc and then come back here.
TypeScript(TS) is a typed superset of JavaScript(JS) that provides compile-time checking of source code and has a bunch of other nice features to native JS
. TS
helps us to fix most bugs and provides us with powerful tools to improve code quality. You can read more about the pros and cons of TS
in other articles, but we’ll go over them a little bit.
The goal here to so understand how TS
works within the React
, and what are the pros and cons of TS
and when we should use it.
So, we should start with creating a project. To create a simple React+TS
project, I recommend using the create-react-app
utility with the template parameter:
$ npx create-react-app --template typescript
After that, you will have a React
project ready to run, in which TS
support is already configured, and you can immediately start writing code.
Let’s introduce a small Button
component, which contains the following logic:
- Handles clicks
- Saves and displays the number of clicks on the button
This simple component, without using
TS
, will be written like this:
import React, { useState } from "react";
const Button = ({ onClick, text }) => {
const [clickCount, setCount] = useState(0);
const handleClick = (event) => {
setCount(clickCount + 1);
onClick(event);
};
return <button onClick={handleClick}>{text}(count: {clickCount})</button>;
};
export default Button;
And here we already have several problems:
- If we don’t pass a function to the component in the prop
onClick
, or pass a different type of data there, the component will break at runtime (in thehandleClick
function), which you might not notice during development. - In the prop
text
we can pass any value, which could lead to unexpected output. Most of the problems are due to the fact that we don’t know what types of data come to us in props. This problem is partly solved by thepropTypes
library, but it has its own limitations — it can only type the component’s props.TS
allows you to cover everything with types:handleClick
function,onClick
function, additional helpers and utilities, stores, and etc. An example of the same component, but usingTS
:
import React, { useState } from "react";
interface IProps {
onClick: (event: React.SyntheticEvent) => void;
text: string;
}
const Button: React.FC<IProps> = ({ onClick, text }) => {
const [clickCount, setCount] = useState(0);
const handleClick = (event: React.SyntheticEvent) => {
setCount(clickCount + 1);
onClick(event);
};
return (
<button onClick={handleClick}>
{text}(count: {clickCount})
</button>
);
};
export default Button;
So, let’s look at the changes. First, we have described the interface for the props. The interface for props is always called IProps. This interface describes the types of props our component accepts. In this example, we described that the component accepts the function onClick: (event: React.SyntheticEvent) => void
and the string field text: string
.
To connect our interface to a component we use the generic type React.FC
, which takes our props interface as an argument React.FC
.
We also type the handleClick
function by describing that it accepts an event with the type React.SyntheticEvent
(documentation).
Now when we use the component we’ll get an error every time we don’t pass the required parameters (onClick
, text
) and this will prevent us from compiling the project.
TS allows you to avoid simple errors with data types that pass through your application and also allows you to structure and strictly define input and output parameters of your functions (Components, Stores and everything else)
In addition to working with components, we often encounter stores. Basically, most of the projects use Redux
as a centralized data store. In this section, we will take a look at an example of a small Redux
store on TS
.
For example, we have a Reducer, which is responsible for our counter (from the component example) and has some actions (increment
, decrement
):
// action types
enum CounterActionTypes {
increment = 'increment',
decrement = 'decrement'
}
// interfaces & types
interface CounterState {
value: number
}
type CounterPayload = number
interface BaseAction<ActionTypes, Payload> {
type: ActionTypes
payload: Payload
}
type CounterAction = BaseAction<CounterActionTypes, CounterPayload>
// actions
const increment = (payload: number): CounterAction => ({
type: CounterActionTypes.increment,
payload
})
const decrement = (payload: number): CounterAction => ({
type: CounterActionTypes.increment,
payload
})
// reducer
const initialState: CounterState = {
value: 0
}
const counterReducer = (
state: CounterState = initialState,
action: CounterAction
): CounterState => {
switch(action.type) {
case CounterActionTypes.increment:
return { ...state, value: state.value + 1 }
case CounterActionTypes.decrement:
return { ...state, value: state.value - 1 }
default:
return state
}
}
export default counterReducer
I intentionally did not divide the code into several different > files, although usually in a real project you keep the code
divided by entities:reducer.ts
,interfaces.ts
,
actions.ts
,sagas.ts
(orepics.ts
).
In the first section, we declare actionTypes
. The enum
from TS
is ideal for this. Enum type — is a data type consisting of a set of named values called elements, members, enumeral, or enumerators of the type. In our case, we use an enum to declare the availability actionTypes
for this reducer. The declaration of actionTypes
is usually found in the file actions.ts
enum CounterActionTypes {
increment = 'increment',
decrement = 'decrement'
}
Then comes the declaration of the types and interfaces we need for the reducer. In this example, I’ve added the BaseAction
interface, which is not usually located directly in each store, but is a common interface used for all actions and is usually separate(for example, in the file store/interfaces.ts
). Then comes the declaration of the interface, which describes the state of the reducer. In our case the reducer stores only one field: value: number
. Also, we declare the CounterPayload = number
type for payloads of all actions that work with this reducer. Finally, the last type is CounterAction
, which uses a generic BaseAction
interface with the necessary parameters. All information about types is usually in the file interfaces.ts
, but it can also be stored next to entities (CounterState
in reducer.ts, CounterPayload
and CounterAction
in actions.ts
)
interface CounterState { value: number }
type CounterPayload = number
interface BaseAction<ActionTypes, Payload> {
type: ActionTypes
payload: Payload
}
type CounterAction = BaseAction<CounterActionTypes, CounterPayload>
The next section is a simple declaration of action creators. These are simple functions that return actions. Thanks to the typing (CounterAction
) we keep all action creators looking the same.
const increment = (payload: number): CounterAction => ({
type: CounterActionTypes.increment,
payload
})
const decrement = (payload: number): CounterAction => ({
type: CounterActionTypes.increment,
payload
})
And, finally, the reducer:
const initialState: CounterState = {
value: 0
}
const counterReducer = (
state: CounterState = initialState,
action: CounterAction
): CounterState => {
switch(action.type) {
case CounterActionTypes.increment:
return { ...state, value: state.value + 1 }
case CounterActionTypes.decrement:
return { ...state, value: state.value - 1 }
default:
return state
}}
In the Reducer, we actively use all the types and interfaces declared above. We use the CounterState
interface to create initialState
, and we use state: CounterState = initialState
and action: CounterAction
as parameters for the reducer. That way, we can’t use something that we didn’t declare in our interfaces. For example, you can’t add a new field to a state without updating the CounterState
interface; after adding it, you’ll have to refactor the cases where the new field isn’t returned and TS
will tell you where and what might be broken. Or, for example, you can’t add a new case to the reducer until you add actionType
to enum CounterActionTypes
. This allows us to make our code robust and bug-proof and protect developers from primitive bugs when working with code.
It is also desirable to write interfaces to work with the API
. Usually, it’s very convenient to describe the response’s
interface. And if your server is written in NodeJS
using TS
, then you can once describe interfaces for reponse’s
and use them both on the server and on the client. It’s very convenient. Small example of working with API
using TS
:
const api = {
posts: "https://jsonplaceholder.typicode.com/posts"
};
export async function request<T>(url: string): Promise<T> {
const response = await fetch(url);
const body = await response.json();
return body;
}
interface Post {
userId: number;
id: number;
title: string;
body: string;
}
type PostResponse = Array<Post>
export async function getPosts(): Promise<PostResponse> {
const posts = await request<PostResponse>(api.posts);
return posts;
}
In this section, we will consider all the advantages of TS
for React
and highlight the main thesis — why and when to use TS
together with React
.
- Reliability. TS allows you to make your application much more robust. You no longer have to worry about calling a function or accessing an object field — typescript will tell you if you made a mistake and won’t let you compile code with an error.
- Easy refactoring. You almost never have to worry about breaking something while refactoring. TS simply will not allow you to break the code.
- IDE support. TS allows you to take autocomplete in your editor to the next level and make it similar to autocomplete in more complex languages(C, Java). Autocompletion, auto-imports, error, and problem highlighting on the fly — all this makes Typescript a great tool for development.
- Patterns. Typescript is a full OOP language that allows you to write OOP-style code. TS implements some useful features, which do not exist and most likely will never exist in native JS. For example, in TS you can use class access modifiers (public, private, protected), decorators, and Dependency Injection. So, if you use TS, you’re getting much closer to popular patterns of modern development than using native JS. Of course, most often these patterns are not applicable to Frontend applications, but some of them are actively used, for example, in Angular. But React also actively uses the advanced features of TS.
- A Large community. Typescript is a mature technology with a huge community, and if you have a problem, just google it and chances are someone has already solved it.
- Open-source. 95% of the libraries and third-party modules are written in Typescript and you should have no problem using them.
- Learning assistance. TS allows you to learn in more than just JS, because it implements many approaches from other languages. If you know and can use Typescript well, it will be much easier for you to learn and use other languages in the future.
If you are developing or planning to develop a large project with long-term support — TS is your go-to choice. Of course, you can also hard-code in TS, but the language itself regularly punishes you for writing bad code. TS will protect you from the primitive bugs and errors and add confidence to your project. But, don’t forget that typescript helps only at the development stage. After compilation, it completely disappears and runtime works in JS with all its advantages and disadvantages. If you’re looking for code quality and have enough time to learn and implement the technology, Typescript is your choice!
< If you choose to use Typescript but exclude unit testing from
< the workflow, this is a very bad pattern. It’s always better to
< choose tests over Typescript, because Typescript tests your
< code, but tests check the business logic, which is much more
< important!
But, as we all know, there is no silver bullet and TS
also has its disadvantages:
- You will have to write a lot of code. On TS you will have to write a lot more code. The amount of code in TS is usually 1.5–2 times higher than in native JS. Accordingly, the time you will spend on the tasks increases proportionally by 1.5–2 times. This is the price for reliability. You have to describe new types and interfaces over and over again and be able to apply them correctly. You’ll also have to spend some time studying the interfaces of external libraries and third-party modules in order to correctly integrate them into the project.
- TS is not for beginners. If your project is planned to be developed by beginner developers (Interns, Juniors), TS is probably not for you. It has a rather high entry threshold. In addition to the complexities of JS, developers will also have to learn the intricacies of Typescipt, which is likely to be very difficult for them.
- You still can write bad code. Yes, this is bad. If something doesn’t work, you can always use //@ts-ignore or any , but by doing this you create problems for yourself in the future. And if your project doesn’t have strict conditional rules described in tsconfig or eslint (for example, don’t use any , cover everything with types), you won’t benefit from TS.
- You will have to declare types for libraries. Even if you don’t have to do it that often, it is quite hard. Especially when the library is not a simple utility, but a whole set of utilities. Fortunately, these days there are almost no untyped libraries left (thanks to DefinitelyTyped)
- Transferring a large production project to TS is difficult. We have tools for that, and you can transfer the project gradually, but all the time you’ll be in pole position and won’t be able to take full advantage of TS. And if you’re also developing features at the same time, the move to TS can drag on for quite a long time.
Finally, TS
is definitely not the right choice for you:
- If you don’t want to write in it or you’re having a hard time writing code in it.
- If your project’s goal is to release the product as soon as possible (
MVP
), thenTS
is also not the best choice for you. You can write the basic version usingJS
, and once the product finds the market fit, rewrite everything usingTS
. - If you have a lot of Junior developers, you may have a problem with TS and you probably shouldn’t use it. At least, you should definitely start with a small one.
- If you already have a big working project in
JS
and you’re planning to rewrite it usingTS
, most likely it’s not worth it. You’d be better off improving your current project code and covering it with tests. This is much more important. - If you have the opportunity to write a type and not use
any
— do it.any
contradicts the main principle of TS — reliability, and exists only to transfer large complex projects toTS
gradually. And even in that case, it’s better to try not to useany
. If possible, try to never use any ever.
In conclusion I would like to say that TS
is a great tool, which is becoming more and more popular every year. And with React
it allows you to add the very reliability and transparency, which are usually missing in Frontend applications. In large corporations, TS
has long been a must-have, but gradually it becomes a must in smaller companies, and hence the number of developers who can use TS
is increasing. Just try learning and using Typescript
on a project and you’ll realize how cool this tool is.
Previously published at maddevs.io/blog
31