Post cover

How to Use TypeScript with React Components

Posted September 29, 2021

In this post, I'm going to discuss why and how to use TypeScript to type React components.

You'll find how to annotate component props, mark a prop optional, and indicate the return type.

1. Why typing React components?

TypeScript is useful if you're coding middle and bigger size web applications. Annotating variables, objects, and functions creates contracts between different parts of your application.

For example, let's say I am the author of a component that displays a formatted date on the screen.


interface FormatDateProps {
date: Date
}
function FormatDate({ date }: FormatDateProps): JSX.Element {
return <div>{date.toLocaleString()}</div>;
}

According to the FormatDateProps interface, the component FormatDate the value of date prop can only be an instance of Date. That is a constraint.

Why is this constraint important? Because the FormatDate component calls the method date.toLocaleString() on the date instance, and the date prop have to be a date instance. Otherwise, the component wouldn't work.

Then the user of the FormatDate component would have to satisfy the constraint, and provide date prop only with Date instances:


<FormatDate
date={new Date()}
/>

If the user forgets about the constraint, and for example provides a string "Sep 28 2021" to date prop:


// Type error:
// Type 'string' is not assignable to type 'Date'.
<FormatDate
date="Sep 28 2021"
/>

then TypeScript will show a type error.

That's great because the error is caught during development, without hiding in the codebase.

2. Typing props

In my opinion, the best benefit React takes from TypeScript is the props typing.

Typing a React component is usually a 2 steps process.

A) Define the interface that describes what props the component accepts using an object type. A good naming convention for the props interface is ComponentName + Props = ComponentNameProps

B) Then use the interface to annotate the props parameter inside the functional component function.

For example, let's annotate a component Message that accepts 2 props: text (a string) and important (a boolean):


interface MessageProps {
text: string;
important: boolean;
}
function Message({ text, important }: MessageProps) {
return (
<div>
{important ? 'Important message: ' : 'Regular message: '}
{text}
</div>
);
}

MessageProps is the interface that describes the props the component accepts: text prop as string, and important as boolean.

Now when rendering the component, you have to set the prop values according to the props type:


<Message
text="The form has been submitted!"
important={false}
/>

Basic Prop Types suggests types for different kinds of props. Use the list as an inspiration.

2.1 Props validation

Now if you happen to provide the component with the wrong set of props, or wrong value types, then TypeScript will warn you at compile time about the wrong prop value.

Usually, a bug is caught in one of the following phases — type checking, unit testing, integration testing, end-to-end tests, bug report from the user — and the earlier you catch the bug, the better!

If the Message component renders with an invalid prop value:


<Message
text="The form has been submitted!"
// Type error:
// Type 'number' is not assignable to type 'boolean'.
important={0}
/>

or without a prop:


<Message
// Type error:
// Property 'text' is missing in type '{ important: true; }'
// but required in type 'MessageProps'.
important={true}
/>

then TypeScript will warn about that.

2.2 children prop

children is a special prop in React components: it holds the content between the opening and closing tag when the component is rendered: <Component>children</Component>.

Mostly the content of the children prop is a JSX element, which can be typed using a special type JSX.Element (a type available globally in a React environment).

Let's slightly change the Message component to use a children prop:


interface MessageProps {
children: JSX.Element | JSX.Element[];
important: boolean;
}
function Message({ children, important }: MessageProps) {
return (
<div>
{important ? 'Important message: ' : 'Regular message: '}
{children}
</div>
);
}

Take a look at the children prop in the interface: it accepts a single element JSX.Element or an array of element JSX.Element[].

Now you can use an element as a child to indicate the message:


<Message important={false}>
<span>The form has been submitted!</span>
</Message>

or multiple children:


<Message important={false}>
<span>The form has been submitted!</span>
<span>Your request will be processed.</span>
</Message>

Challenge: how would you update the MessageProps interface to support also a simple string value as a child? Write your solution in a comment below!

2.3 Optional props

To make a prop optional in the props interface, mark it with a special symbol symbol ?.

For example, let's mark the important prop as optional:


interface MessageProps {
children: JSX.Element | JSX.Element[];
important?: boolean;
}
function Message({ children, important = false }: MessageProps) {
return (
<div>
{important ? 'Important message: ' : 'Regular message: '}
{children}
</div>
);
}

Inside MessageProps interface the important prop is marked with an ?important?: boolean — making the prop optional.

Inside the Message function I have also added a false default value to the important prop: { children, important = false }. That's going to be the default value in case if important prop is not indicated.

Now TypeScript allows you to skip the important prop:


<Message>
<span>The form has been submitted!</span>
</Message>

Of course, you can still use important if you'd like to:


<Message important={true}>
<span>The form has been submitted!</span>
</Message>

3. Return type

In the previous examples Message function doesn't indicate explicitly its return type. That's because TypeScript is smart and can infer the function's return type — JSX.Element:


// MessageReturnType is JSX.Element
type MessageReturnType = ReturnType<typeof Message>;

In the case of React functional components the return type is usually JSX.Element:


function Message({
children,
important = false
}: MessageProps): JSX.Element {
return (
<div>
{important ? 'Important message: ' : 'Regular message: '}
{children}
</div>
);
}

There are cases when the component might return nothing in certain conditions. If that's the case, just use a union JSX.Element | null as the return type:


interface ShowTextProps {
show: boolean;
text: string;
}
function ShowText({ show, text }: ShowTextProps): JSX.Element | null {
if (show) {
return <div>{text}</div>;
}
return null;
}

ShowText returns an element if show prop is true, otherwise returns null. That's why the ShowText function's return type is a union JSX.Element | null.

3.1 Tip: enforce the return type

My recommendation is to enforce each function to explicitly indicate the return type. Many silly mistakes and typos can be caught by doing so.

For example, if you have set accidently a newline between return and the returned expression, then the explicitly indicated return type would catch this problem:


function BrokenComponent(): JSX.Element {
// Type error:
// Type 'undefined' is not assignable to type 'Element'.
return
<div>Hello!</div>;
}

(Note: when there's a newline between the return keyword and an expression, then the function returns undefined rather than the expression.)

However, if there's no return type indicated, the incorrectly used return remains unnoticed by TypeScript (and by you!):


function BrokenComponent() {
return
<div>Hello!</div>;
}

Then good luck debugging!

4. Conclusion

React components can greatly benefit from TypeScript.

Typing components is especially useful to validate the component props. Usually, that's performed by defining an interface where each prop has its type.

Then, when the annotated component renders, TypeScript verifies if correct prop values were supplied.

On top of data validation, the types can be a great source of meta-information with clues of how the annotated function or variable works.

Like the post? Please share!

Dmitri Pavlutin

About Dmitri Pavlutin

Software developer and sometimes writer. My daily routine consists of (but not limited to) drinking coffee, coding, writing, overcoming boredom 😉. Living in the sunny Barcelona. 🇪🇸