Basics of props typing in React

This article is intended for those who are just starting to write their React applications in TypeScript, and is also a reminder for me, because recently I was confused about the typing of children props.

Let me start with the fact that there are tasks and projects for which TypeScript is not needed. For example, this is a one-time project that, if it will scale or change over time, then it is not significant, or a project that does not have much data received from the backend, and for the most part the data is static or hardcoded.

If you feel that despite the fact that the project does not have complex logic, but the component tree and the number and variation of props transferred are impressive, I would use prop-types. Previously, this feature was part of React and was used like this: React.PropTypes. But starting with React 15.5, it has moved to a separate library, so now it needs to be installed as, for example, an npm/yarn package. It is used to validate prop types in React components. These are all TS features, but for a project with a large number of components and props, this is what you need. The syntax for describing prop types differs from TS.

Typing props via prop-types

import PropTypes from 'prop-types';

const Component = ({ name, age, isActive }) => (
  <div>
    <p>Name: {name}</p>
    <p>Age: {age}</p>
    <p>Active: {isActive ? 'Yes' : 'No'}</p>
  </div>
);

Component.propTypes = {
  name: PropTypes.string.isRequired,
  age: PropTypes.number.isRequired,
  isActive: PropTypes.bool,
};

export default Component;

PropTypes

  • PropTypes.any: Any value.

  • PropTypes.bool: Boolean value.

  • PropTypes.number: Number.

  • PropTypes.string: Line.

  • PropTypes.func: Function.

  • PropTypes.array: Array.

  • PropTypes.object: An object.

  • PropTypes.symbol: Symbol.

  • PropTypes.node: Something that can be rendered (a number, a string, an element, an array, etc.).

  • PropTypes.element: React element.

  • PropTypes.instanceOf(Class): An instance of a particular class.

  • PropTypes.oneOf(['Option1', 'Option2']): One of the specified values.

  • PropTypes.oneOfType([PropTypes.string, PropTypes.number]): One of the specified types.

  • PropTypes.arrayOf(PropTypes.number): An array of elements of a certain type.

  • PropTypes.objectOf(PropTypes.number): An object with values ​​of a certain type.

  • PropTypes.shape({ name: PropTypes.string, age: PropTypes.number }): An object with a given structure.

  • PropTypes.exact({ name: PropTypes.string, age: PropTypes.number }): An object with a precisely defined structure (additional properties are prohibited).

TypeScript

We are gradually moving on to TypeScript. It is time to connect it when the complexity and amount of logic grows, and a lot of data starts coming from the backend. When all the data is received from the backend, then TypeScript is definitely the only one. Yes, it will not help in production to somehow process the wrong type of data that got into props. Its power is different – your project will crash at the compilation stage.

The terms runtime and compile time

There are two processes of code execution:

  • Runtime (this is when we open our application in the execution environment (for example, in a browser), it begins to render, and also when the user begins to interact with it – through the attached event handlers).

  • Compile time (this is when during development, to convert TS to JS, the TS server is launched, and the moment of conversion (compilation) of TS code to JS is called compile time. As a rule, during development in dev mode, compile time is launched after saving the ts/tsx file with changes. Also, in the IDE, code parsing is constantly happening, and when we start passing the wrong type to something typed (for example, to a component with typed props) – the code editor highlights the variable that has a type different from the expected one).

So, when there are errors in PropTypes, our project does not crash, and the problems are displayed in the console. To the obvious advantages of working on TypeScript compared to PropTypes, I will add the ability to integrate with the IDE: type hints and code autocompletion. As a rule, they work by default for everyone after installing TypeScript. Even better code documentation: types serve as documentation for component props. This is extremely important for projects with a bunch of HTTP requests. You can type everything in TS, not just props, which is extremely important for large and complex projects with global variables (enum to the rescue), type extensions (and interfaces), utility types, and dynamic types (Generics). Without all these tools, you cannot build a complex and at the same time reliable application. If I were to describe in one sentence why TS is needed in a project, I would answer: with TypeScript, support, development, and refactoring of code becomes much less risky and more predictable.

In TS, props typing is usually described in the interface (it can be in type, but I have not seen this often). I will only give examples here for functional components, since the article is intended for beginners, and they, as a rule, already write in functional ones. In the example below, I use props destructuring directly in the parameters of the arrow function:

const Component = ({ name, age, isActive }) => (...);

In some projects you may encounter this approach:

typescriptКопировать кодconst Component = (props) => {
  const { name, age, isActive } = props;
  return (...);
};

Someone is using props.name.

The method of accessing props is not important, the created interface must be passed as a generic to the FC type of the React object – React.FC<ComponentProps>.

import React from 'react'; // с React 17 для создания компонента этот импорт не обязателен, но если мы используем React.FC - то нужно

interface ComponentProps {
  name: string;
  age: number;
  isActive?: boolean; // ? - необязательный пропс
}

const Component: React.FC<ComponentProps> = ({ name, age, isActive }) => (
  <div>
    <p>Name: {name}</p>
    <p>Age: {age}</p>
    <p>Active: {isActive ? 'Yes' : 'No'}</p>
</div>
);

export default Component;

There is a popular and generally accepted convention to add the postfix “Props” to the name of such interfaces. This name must be passed as a generic to the FC method of the React object.
You can write it like this: React.FC<ComponentProps>.

You can import the FC (FunctionComponent) type directly:

import { FC } from 'react';

interface ComponentProps {
  name: string;
  age: number;
}

const MyComponent: FC<ComponentProps> = ({ name, age }) => (
  <div>
    <p>Name: {name}</p>
    <p>Age: {age}</p>
  </div>
);

export default MyComponent;

The second way to type props is not to use the FC type, but to specify it in the parameters of the arrow function – ({ name, age }: ComponentProps) => { return (...)}

interface ComponentProps {
  name: string;
  age: number;
}

const Component = ({ name, age }: ComponentProps) => (
  <div>
    <p>Name: {name}</p>
    <p>Age: {age}</p>
  </div>
);

export default Component;

The difference with this typecasting is that React.FC sneakily gives us the children typecasting by default (it types it as React.ReactNode – we'll talk about this type below). This can be convenient in some cases, but it can also be confusing if you don't plan to use children in your component or need to specify its type more specifically. When using React.FC, TypeScript adds children to your component's props by default. This means that your component will expect children even if you don't use them.

React.FC usage example

import { FC } from 'react';

interface MyComponentProps {
  name: string;
  age: number;
}

const MyComponent: FC<MyComponentProps> = ({ name, age, children }) => (
  <div>
    <p>Name: {name}</p>
    <p>Age: {age}</p>
    <div>{children}</div> {/* children автоматически типизированы */}
</div>
);

export default MyComponent;

Because of this feature React.FCwhen using this syntax to add typing to props, it is good practice to explicitly type childrenif they are assumed:

import { FC, ReactElement } from 'react';

interface ComponentProps {
  name: string;
  age: number;
  children?: ReactElement; // Явно указываем тип для children
}

const MyComponent: FC<ComponentProps> = ({ name, age, children = null }) => (
  <div>
    <p>Name: {name}</p>
    <p>Age: {age}</p>
    {children} {/* children автоматически типизированы */}
  </div>
);

export default MyComponent;

It is also a good practice to assign a default value to optional props, as shown in the example above (children = null). This helps to avoid errors if the props were not passed.

Typification of children

Now about prop typing childrenthat is, child components that are passed between JSX tags, for example: <Component>{children}</Component> or <Component><SomeComponent /></Component>There are two popular ways to type them, and each of them is needed for different tasks:

  1. The most versatile – React.ReactNode: Use it if you need flexibility in transferring in childrenas it covers all types: strings, numbers, booleans, fragments (arrays of JSX elements), null, undefinedand ReactElementFor example, in a modal window component, you can pass both a string and a component.

    import React, { ReactNode } from 'react';
    
    interface ModalProps {
      title: string;
      children?: ReactNode; // Универсальный тип для children
    }
    
    const Modal: React.FC<ModalProps> = ({ title, children }) => (
      <div className="modal">
        <h1>{title}</h1>
        <div>{children}</div>
      </div>
    );
    
    export default Modal;
  2. ReactElement: Use this type when you only allow JSX (React) elements and components to be passed in. It is a more limited type than React.ReactNode.

    import React, { ReactElement } from 'react';
    
    interface ButtonProps {
      label: string;
      children?: ReactElement; // Только React элементы
    }
    
    const Button: React.FC<ButtonProps> = ({ label, children }) => (
      <button>
        {label}
        {children}
      </button>
    );
    
    export default Button;

Note: JSX.Element also exists as a type, but is rarely used in practice, since TypeScript already implicitly types the value returned from the component. For example:

const Component = (): JSX.Element => (
  <div>
    Hello, World!
  </div>
);

export default Component;

Conclusion

Typing props in React helps to create a more reliable and predictable application. Using PropTypes is useful in less complex projects, while TypeScript offers more powerful tools for working with large and complex code bases. TypeScript not only simplifies code refactoring, but also provides a rich set of tools for working with types, which makes code development, maintenance, and refactoring more convenient and safe.

Thanks for reading! I hope I helped someone.
I would be glad to receive any comments and clarifications from you.
Good luck, workers!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *