Typing functions with TypeScript. How to do it properly in ReactJS?
When I started learning Typescript with React I was able to stick with the convention of defining components with a const
keyword. After some I switched teams and we decided to use function declarations as a convention for defining React components.
And then the problem appeared to me. While using arrow functions it was pretty straightforward to start typing, with TypeScript, components in React, the way of typing function declarations was completely different to me and unknown.
Function expressions vs function declarations. What is the difference?
At first glance, there are no difference on how both types of functions work in general. The visible difference is a different annotation.
Function expression
const getTimestamp = () => {
return Date.now();
};
Function declaration
function getTimestamp() {
return Date.now();
}
Other than that there are practically no meaningful differences now, except hoisting. It's just a convention you or your team picks in order to keep codebase consistent.
Typing components as function expressions in TypeScript
Let's take a look at typing function expressions. It's the most common approach, when developing React code.
import * as React from 'react';
interface Props {
title: string;
}
const SomeComponent: React.FC<Props> = ({title}) => {
return <div>SomeComponent body</div>;
}
As you can see it's pretty straightforward. You're defining:
- Component props types with an
interface
keyword (you might also usetype
if you prefer). - Next, you're declaring that
SomeComponent
has the type of React'sFunctionComponent
. - You don't need to type the function's output as it's already defined by usage of
React.FC
.
This way you could type your component properly, which is just enough for simple use cases.
Typing components as function declarations in TypeScript
import * as React from 'react';
interface Props {
title: string;
}
function SomeComponent({title}: Props): JSX.Element => {
return <div>SomeComponent body</div>;
}
You might have noticed, that notation is different. You're not using React.FC
definition anymore and you have to type the output, hence TypeScript will know the type of your components when using it somewhere in a codebase.
I have typed the output as of JSX.Element
type. I'm explaining why in the next section.
What's the difference between JSX.Element, ReactNode and ReactElement?
In this section I'm going to explain you the difference between JSX.Element, ReactNode and ReactElement.
TLDR; in most cases you'll want to use JSX.Element type as function's output type.
JSX.Element and ReactElement
They're almost the same, but with slight difference. While ReactElement is an object with a type and props, the JSX.Element is a ReactElement with the generic type for props and type being any
.
Here's the detailed definition for ReactElement:
interface ReactElement<P = any, T extends string | JSXElementConstructor<any> = string | JSXElementConstructor<any>> {
type: T;
props: P;
key: Key | null;
}
And the definition of JSX.Element:
declare global {
namespace JSX {
interface Element extends React.ReactElement<any, any> { }
}
}
As you can see JSX is a global namespace that gets definition of Element from ReactElement. Each library can define it differently. In React, it's defined like above.
ReactNode
On the other hand, ReactNode
is less strict than the two above. It represents the following: {} | undefined | null
and that's the reason it's used as a type of React children. You don't know whether children exist or not and what is their actual type.
Summary
With the knowledge presented in the article you'll be able to developer error-proof web apps with ease. I hope that now you get how to type function expressions and function declarations properly in React and understand the difference between JSX.Element, ReactElement and ReactNode. I wanted to share with you that knowledge because I remember the time when I had such doubts how to type React components in a correct manner.