There is no single “right” way of writing React code using TypeScript.
As with other technologies, if your code compiles and works, you probably did something right.
So, here I’m going to list some useful code-snippets that follow said “best practices”. There are a lot of them, some that you might’ve used already in the past and some that might be new. Just go through the list and make mental notes. Bookmarking this article for future reference might be a good idea as well.
By building projects using React with TS, you make sure your components are easily comprehensible to other developers (as well as to your future self). That is absolutely crucial for making them ready for sharing. It’s a great way to write maintainable code and optimize your team collaboration.
Read Also: What’s New Features of TypeScript 3.8
$ npx create-react-app your-app-name --template typescript
If you’re more of a fan of Yarn, you can use the following command:
$ yarn create react-app your-app-name --template typescript
In either case, notice how we’re not directly using the app, rather, we’re using other tools that will download the latest version of the app whenever it’s required. This helps ensure you’re not using an outdated version.
Read Also: React-Redux with TypeScript
Some of the very interesting tidbits added by TS to the language are:
One of the many benefits TypeScript brings to the table, is access to constructs such as this, which allows you to define the interface of your components or even any other complex objects you might want to use with them, such as the shape your Props
object will have (i.e how many properties and their types).
import React from 'react';
interface IButtonProps {
/** The text inside the button */
text: string,
/** The type of button, pulled from the Enum ButtonTypes */
type: ButtonTypes,
/** The function to execute once the button is clicked */
action: () => void
}
const ExtendedButton : React.FC<IButtonProps> = ({text, type, action}) => {
}
The above code ensures that whoever uses your components needs to add exactly 3 properties:
Read Also: 6 Front-End Challenges in 2020
Note that we “extended” the FC (Functional Component) type with our own custom interface. That gives our function all the generic functional component definitions such as the ‘children’ prop and a return type that must be assignable to JSX.Element.
If you ignore one of them or send something that’s not compatible, both the TypeScript compiler and your IDE (assuming you’re using a JavaScript specific IDE, such as Code) will notify you and won’t allow you to continue until you fix it.
A better way to define our ExtendedButton element would be to extend a native HTML button element type like so:
import React, {ButtonHTMLAttributes} from 'react';
interface IButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
/** The text inside the button */
text: string,
/** The type of button, pulled from the Enum ButtonTypes */
type: ButtonTypes,
/** The function to execute once the button is clicked */
action: () => void
}
const ExtendedButton : React.FC<IButtonProps> = ({text, type, action}) => {
}
const ExtendedButton : React.FC<IButtonProps> = ({text, type, action} : IButtonProps) => {
}
(The props are defined directly and explicitly using :IButtonProps
in addition to defining the component with :React.FC<IButtonProps>
)
Read Also: JavaScript Basics Before You Learn React
Just like with Interfaces, Enums allow you to define a set of related constants as part of a single entity.
//...
/** A set of groupped constants */
enum SelectableButtonTypes {
Important = "important",
Optional = "optional",
Irrelevant = "irrelevant"
}
interface IButtonProps {
text: string,
/** The type of button, pulled from the Enum SelectableButtonTypes */
type: SelectableButtonTypes,
action: (selected: boolean) => void
}
const ExtendedSelectableButton = ({text, type, action}: IButtonProps) => {
let [selected, setSelected] = useState(false)
return (<button className={"extendedSelectableButton " + type + (selected? " selected" : "")} onClick={ _ => {
setSelected(!selected)
action(selected)
}}>{text}</button>)
}
/** Exporting the component AND the Enum */
export { ExtendedSelectableButton, SelectableButtonTypes}
Importing and using Enums:
import React from 'react';
import './App.css';
import {ExtendedSelectableButton, SelectableButtonTypes} from './components/ExtendedSelectableButton/ExtendedSelectableButton'
const App = () => {
return (
<div className="App">
<header className="App-header">
<ExtendedSelectableButton type={SelectableButtonTypes.Important} text="Select me!!" action={ (selected) => {
console.log(selected)
}} />
</header>
</div>
);
}
export default App;
Please note that unlike Interfaces or Types, Enums will get translated into plain JavaScript. So, for example, this:
enum SelectableButtonTypes {Important = "important",Optional = "optional",Irrelevant = "irrelevant"}
will transform into this:
"use strict";var SelectableButtonTypes;(function (SelectableButtonTypes) {SelectableButtonTypes["Important"] = "important";SelectableButtonTypes["Optional"] = "optional";SelectableButtonTypes["Irrelevant"] = "irrelevant";})(SelectableButtonTypes || (SelectableButtonTypes = {}));
Read Also: Angular vs React vs Vue: Which one will be popular in 2020
A common question that newcomers to TypeScript have is whether they should be using Interfaces or Type Aliases for different parts of their code — after all, the official documentation is a bit unclear regarding that topic.
Truth is, although these entities are conceptually different, in practice, they are quite similar:
//extending interfaces
interface PartialPointX { x: number; }
interface Point extends PartialPointX { y: number; }
//extending types
type PartialPointX = { x: number; };
type Point = PartialPointX & { y: number; };
// Interface extends type
type PartialPointX = { x: number; };
interface Point extends PartialPointX { y: number; }
//Type alias extends interfaces
interface PartialPointX { x: number; }
type Point = PartialPointX & { y: number; };
2. They can both be used to define the shape of objects.
//defining the interface for objects
interface Point {
x: number;
y: number;
}
//using types as well
type Point2 = {
x: number;
y: number;
};
Read Also: Push Notifications in PWA Using Firebase and React
3. They both can be implemented in the same way.
//implementing the Interface
class SomePoint implements Point {
x: 1;
y: 2;
}
//Implementing the Type alias
class SomePoint2 implements Point2 {
x: 1;
y: 2;
}
type PartialPoint = { x: number; } | { y: number; };
// This is the only thing you can't do: implement a union type
class SomePartialPoint implements PartialPoint {
x: 1;
y: 2;
}
The only extra feature Interfaces bring to the table (that Type aliases don’t), is “declaration merging” which means you can define the same interface several times and with each definition, the properties get merged:
interface Point { x: number; } //declaration #1
interface Point { y: number; } //declaration #2
// These two declarations become:
// interface Point { x: number; y: number; }
const point: Point = { x: 1, y: 2 };
Read Also: Building Live Streaming App with Node.js and React
Part of the benefits of using Interfaces is that you’re able to enforce the properties of your props for your components. However, thanks to the optional syntax available through TypeScript, you can also define optional props, like this:
//...
interface IProps {
prop1: string,
prop2: number,
myFunction: () => void,
prop3?: boolean //optional prop
}
//...
function MyComponent({...props}: IProps) {
//...
}
/** You can then use them like this */
<mycomponent prop1="text here" prop2=404 myFunction={() = {
//...
}} />
<mycomponent prop1="text here" prop2={404} myFunction={() = {
//...
}} prop3={false} />
Hooks are the new mechanics React provides to interact with several of its features (such as the state) without the need to define a class.
Hooks such as useState
receive a parameter and correctly return the state (again, that’s for this case) and a function to set it.
Thanks to TypeScript’s type validation, you can enforce the type (or interface) of the initial value of the state, like this:
const [user, setUser] = React.useState<IUser>(user);
However, if the initial value for your hook can potentially be a null
, then the above example will fail. For these cases, TypeScript allows you to set an optional type as well, making sure you’re covered from all sides.
const [user, setUser] = React.useState<IUser | null>(null);
// later...
setUser(newUser);
That way you’re ensuring you keep type checks, but allow for those scenarios where the initial value can come as null
.
Much like the way you define generic functions and interfaces in TypeScript, you can define generic components, allowing you to re-use them for different data types. You can do this for props and states as well.
interface Props<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
}
function List<T>(props: Props<T>) {
const { items, renderItem } = props;
const [state, setState] = React.useState<T[]>([]);
return (
<div>
{items.map(renderItem)}
</div>
);
}
You can then use the component either by taking advantage of type inference or directly specifying the data types, likes so:
ReactDOM.render(
<List
items={["a", "b"]} // type of 'string' inferred here
renderItem={item => (
<li key={item}>
{item.trim()} //allowed, because we're working with 'strings' all around
</li>
)}
/>,
document.body
);
ReactDOM.render(
<List<number>
items={[1,2,3,4]}
renderItem={item => <li key={item}>{item.toPrecision(3)}</li>}
/>,
document.body
);
For the latter, note that if your list contains strings instead of numbers, TypeScript will throw an error during the transpilation process.
Sometimes, your components function and behave like native HTML elements (on steroids). For example, a “borederd box” (which is simply a component that always renders a div
with a default border) or a “big submit” (which again, is nothing but your good old submit button with a default size and maybe some custom behavior).
For these scenarios, it’s best to define your component type as a native HTML element or an extension of it.
export interface IBorderedBoxProps extends React.HTMLAttributes<HTMLDivElement> {
title: string;
}
class BorderedBox extends React.Component<IBorderedBoxProps, void> {
public render() {
const {children, title, ...divAttributes} = this.props;
return (
//it is a DIV afterall, and we're trying to let the user use this component knowing that.
<div {...divAttributes} style={{border: "1px solid red"}}>
<h1>{title}</h1>
{children}
</div>
);
}
}
const myBorderedBox = <BorderedBox title="Hello" onClick={() => alert("Hello")}/>;
As you can see, I’ve extended HTML’s default props and added a new one: “title” for my specific needs.
As you probably know, React provides its own set of events, which is why you can’t directly use the good old HTML Events. That being said, you do have access to all the useful UI events you need, so much so in fact, that they have the same names as well, so make sure you reference them directly like React.MouseEvent
or just remember to import them from React like so:
import React, { Component, MouseEvent } from 'react';
The benefits of using TypeScript here, is that we can also use Generics (like in the previous example) to restrict the elements a particular event handler can be used on.
For example, the following code will not work:
function eventHandler(event: React.MouseEvent<HTMLAnchorElement>) {
console.log("TEST!")
}
const ExtendedSelectableButton = ({text, type, action}: IButtonProps) => {
let [selected, setSelected] = useState(false)
return (<button className={"extendedSelectableButton " + type + (selected? " selected" : "")} onClick={eventHandler}>{text}</button>)
}
And you’ll see an error message similar to the following:
You can, however, use unions to allow a single handler to be re-used by multiple components:
/** This will allow you to use this event handler both, on anchors and button elements */
function eventHandler(event: React.MouseEvent<HTMLAnchorElement | HTMLButtonElement>) {
console.log("TEST!")
}
Finally, for the last tip, I wanted to mention React’s index.d.ts and the global.d.ts files. They’re both installed when you add React to your project (if you used npm, you’ll find them inside the npm_modules/@types/react folder.
These files contain type and interface definitions used by React, so if you need to understand the props of one particular type (or simply which types React makes available), you can open these files and review their content.
For example:
There you can see a small section of the index.d.ts file, showing the different signatures for the createElement
function.
There is a lot more you can achieve by using TypeScript as part of your React toolchain.
Either way, I hope you got something out of this article, and feel free to leave any other tips or tricks you’ve picked up over the years of using TypeScript for your React projects!
☞ Using Typescript with modern React (i.e. hooks, context, suspense)
☞ Top 11 React UI Component Libraries In 2018
☞ React Fundamentals for Beginners
☞ Learn the basics of React and using NPM
☞ Learn React 2019 - Full Course for Beginners - React 2019 Tutorial