48
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
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: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:
onClick
, or pass a different type of data there, the component will break at runtime (in the handleClick
function), which you might not notice during development.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 the propTypes
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 using TS
:
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
(or epics.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
.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!
< 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:Finally,
TS
is definitely not the right choice for you:MVP
), then TS
is also not the best choice for you. You can write the basic version using JS
, and once the product finds the market fit, rewrite everything using TS
. JS
and you’re planning to rewrite it using TS
, 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.any
— do it. any
contradicts the main principle of TS — reliability, and exists only to transfer large complex projects to TS
gradually. And even in that case, it’s better to try not to use any
. 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
48