Utilisez TypeScript avec React

Si vous avez appris à développer un site avec React et que vous venez de découvrir TypeScript, la suite logique est relativement simple : utiliser TypeScript avec React !

La bonne nouvelle est que l’intégration de ces deux technologies dans un même projet est plutôt simple. Mais il y a quand même quelques pièges à éviter et des astuces à connaître !

Voyons tout ça ensemble.

Configurez votre projet React

Pour pouvoir profiter de TypeScript avec React, il est nécessaire d’installer deux ou trois choses et de configurer TypeScript pour qu’il comprenne le JSX.

Nous allons faire ça étape par étape, pas de panique !

Installez la définition des types

La première chose à faire est d’installer la définition des types de React via la commande :

npm install @types/react @types/react-dom

Et c’est tout !

@types/react et @types/react-dom indiquent à TypeScript quels sont les types définis, utilisés et exposés par React.

Votre projet est déjà presque prêt !

Configurez le compilateur TypeScript

Ouvrez maintenant le fichier tsconfig.jsonpour modifier lescompilerOptions.

Assurez-vous que le tableau lib contienne bien la valeurdom.

La propriété jsx quant à elle doit être présente et avoir une valeur parmi celles indiquées dans la documentation.

La valeurpreservedevrait suffire pour vos premiers tests, mais sachez que react-jsx est généralement conseillée pour générer un code un peu plus optimisé (elle est recommandée depuis la version 17 de React car elle évite d’avoir besoin d’importerReact partout). Faites selon vos besoins !

Retrouvez ci-dessous un exemple de tsconfig.json contenant le minimum syndical pour un projet React :

{
  "compilerOptions": {
    // Version de JavaScript générée par TypeScript
    "target": "ES2020",

    // Bibliothèques TypeScript à inclure
    // - "dom" pour les API navigateur (document, window, etc.)
    // - "esnext" pour les dernières fonctionnalités JS
    "lib": ["dom", "esnext"],

    // Mode de résolution des modules pensé pour les projets modernes
    "moduleResolution": "bundler",

    // Indique à TypeScript comment gérer le JSX
    // "preserve" : conserve le JSX tel quel pour que Babel/Webpack/Vite le transforme ensuite
    // "react-jsx" : recommandé pour React 17+
    "jsx": "preserve",

    // Active les vérifications strictes du typage
    "strict": true
  },
  // Dossier(s) à inclure dans la compilation
  "include": ["src"]
}

Renommez les fichiers contenant les composants

Vos composants React sont probablement définis dans des fichiers ayant l’extension .jsx

Pour utiliser TypeScript dans ces fichiers, il faut renommer leur extension en .tsx

Et c’est tout !

Une fois toutes ces configurations effectuées, nous pouvons entrer dans le vif du sujet : typer un projet React.

Typez un composant (c’est comme typer une fonction)

Découvrez comment les props sont définies avec TypeScript

Commençons par prendre ce composant  Hello  très basique :

function Hello({ name }) {
	return <p>Hello {name}!</p>
}

export default Hello;

Ici, on aimerait que namen’accepte que des valeurs de typestring.

Si on avait à faire à une simple fonction, on aurait procédé ainsi :

function Hello({ name }: { name: string }) {
	return <p>Hello {name}!</p>
}

export default Hello;

Mais ici, on a un composant React, et non une simple fonction, donc le typage s’écrit ainsi :

function Hello({ name }: { name: string }) {
	return <p>Hello {name}!</p>
}

export default Hello;

Hmm… Je ne vois pas de différence, c'est normal ?

Oui ! Un composant React est une simple fonction : définir les types des props d’un composant est donc aussi facile que définir les types des paramètres d’une fonction.

De plus, TypeScript est capable de déduire le type de retour des fonctions. Comme on a installé@types/react lors de la configuration du projet, il peut donc comprendre tout seul que notre fonction retourne du JSX… et donc qu’elle peut être utilisée comme un composant React !

Petite subtilité quand même si vous voulez faire les choses proprement : il est préférable de définir le type des props dans une interface dédiée :

export interface HelloProps {
	name: string;
}

function Hello({ name }: HelloProps) {
	return <p>Hello {name}!</p>
}

export default Hello;

Cela permet de bien séparer la définition des types du reste du composant.

Notez en outre le export : il permet à votre fichier d’exposer à la fois le composant et ses props ; ce qui peut être utile pour les réutiliser, comme dans l’exemple ci-dessous.

/* Fichier Hello.tsx */
export interface HelloProps {
	name: string;
};

function Hello({ name }: HelloProps) {
	return <p>Hello {name}!</p>
}

export default Hello;

/////////////////

/* Fichier ShowAge.tsx */
// On import le composant Hello et le type définissant ses props
import Hello, { type HelloProps } from './Hello';

// ShowAgeProps étend HelloProps, ce qui permet
// d'éviter la répétition de la définition de "name"
export interface ShowAgeProps extends HelloProps {
    age: number;
}

function ShowAge({ name, age }: ShowAgeProps) {
	return (
	  <div>
		  <Hello name={name} />
		  <p>Your age: {age}</p>
	  </div>
	);
}

export default ShowAge;

Pourquoi utiliser interfaceet nontype ?

Honnêtement, ça ne change pas grand-chose, et c’est principalement une question de préférence.

Mais si votre projet contient beaucoup de code et que vous souhaitez ré-utiliser vos types pour en définir d’autres, sachez que typesera moins performant que interface (TypeScript sera un peu plus lent pour traiter les types de votre projet). Consultez cet article (en anglais) pour en savoir plus.

Utilisez des props discriminantes pour gérer les cas complexes

Il peut arriver qu’un composant ait des props qui ne fonctionnent que si certaines autres sont renseignées, ou au contraire qui se “contredisent”. Prenons cet exemple :

interface ResponseStatusProps {
	status: 'success' | 'error',
	errorReason?: string;
}

function ResponseStatus({ status, errorReason }: ResponseStatusProps) {
	// Peu importe ce que fait le composant
}

Ici, le composant ResponseStatuspeut avoir un statusen succès ou en erreur.

Il a également une props errorReason qui, on le devine, servira à afficher la raison de l’erreur obtenue. Cette props est optionnelle (notez le? dans l’interface), et pour cause : ça n’a pas de sens de renseigner uneerrorReason si le status estsuccess!

Ça serait bien qu’on puisse rendre obligatoire errorReason si, et seulement si,status esterror, non ?

Et bien c’est tout à fait possible avec TypeScript ! 

// Cas 1 : status = 'success', pas d'errorReason
interface SuccessProps {
  status: 'success';
  errorReason?: never; // interdit toute valeur
}

// Cas 2 : status = 'error', errorReason obligatoire
interface ErrorProps {
  status: 'error';
  errorReason: string;
}

// On crée le type final avec une union
type ResponseStatusProps = SuccessProps | ErrorProps;

function ResponseStatus({ status, errorReason }: ResponseStatusProps) {
 	// Peu importe ce que fait le composant
}

// ✅ Correct
<ResponseStatus status="success" />;
<ResponseStatus status="error" errorReason="Connexion perdue" />;

// ❌ Erreurs détectées par TS
// errorReason interdit si on est en "success"
<ResponseStatus status="success" errorReason="..." />;
// errorReason manquant alors qu'on est en "error"
<ResponseStatus status="error" />; 

À propos de  React.FC 

En parcourant du code sur Internet ou dans de vieux tutoriels, vous tomberez peut-être sur des composants écrits comme ceci :

const Hello: React.FC<{ name: string }> = ({ name }) => {
  return <p>Hello {name}!</p>;
};

Ici, React.FC (pour Function Component) est un type fourni par React qui permet :

  • de typer automatiquement les props (et d’y inclure par défaut children)

  • de donner un typage générique aux composants fonctionnels

À la sortie de TypeScript avec React, React.FC semblait très pratique notamment parce qu’il permettait d’éviter la déclaration dechildren.

Cela dit, on évite aujourd’hui de s’en servir, car cette définition implicite de children n’est pas toujours souhaitable : cela peut faire croire qu’un composant accepte des enfants alors que non !

De plus, TypeScript est capable aujourd’hui de déduire bien plus efficacement qu’avant les types de nos composants, rendant React.FC pour ainsi dire inutile tout en étant moins flexible.

Utilisez les types spécifiques à React

React propose différents types TypeScript pour vous aider à rendre votre application plus robuste. Il y en a beaucoup, et tous les lister serait inutile.

Voici les principaux à connaître, ce qui devrait couvrir la majorité de vos besoins.

ReactNode: pour tout ce qui peut être rendu par React

On s’en sert principalement pour la prop children, mais ce type est utile pour tout ce qui peut contenir du JSX, c’est-à-dire du texte (string), des nombres (number), du JSX, des fragments,nullundefined, etc.

import type { ReactNode } from 'react';

// Props
interface MyComponentProps {
	children: ReactNode;
}

// Utilisations correctes
<MyComponent>Test</MyComponent>
<MyComponent><p>Test</p></MyComponent>
<MyComponent>
	{myArray.map(item => <p key={item.id}>{item.name}</p>)}
</MyComponent>

ReactElement: pour demander spécifiquement un élément React

On s’en sert quand une valeur doit contenir exactement un élément React, c’est-à-dire pas du texte, pas un tableau ou autre chose. C’est donc un type qui est plus strict que  ReactNode.  En général, on préfère utiliserReactNode mais il peut être utile de savoir queReactElement existe.

import type { ReactElement } from 'react';

// Props
interface MyComponentProps {
	header: ReactElement;
}

// Utilisation correcte
<MyComponent header={<h1>My header</h1>} />

ElementType: pour demander un composant React ou une balise HTML native

On s’en sert quand on souhaite que notre composant React puisse instancier un autre composant React.

import type { ElementType, ReactNode } from 'react';

// Props
interface ContainerProps {
	as: ElementType;
	children: ReactNode;
}

// ElementType accepte tous les noms de balises HTML natives
<Container as="div">Test</Container>
// ElementType peut aussi contenir n'importe quel composant React
// (on admet ici que Box est un composant que l'on a défini)
<Container as={Box}>Test</Container>

Définissez le style “inline” avecCSSProperties

Il est possible avec React de définir un style “inline” pour chacun de nos éléments React,et ce via la propstyle. Même si ce n’est pas recommandé, il est parfois utile de pouvoir le faire.

Il est donc utile de pouvoir le typer !

import type { CSSProperties, ReactNode } from 'react';

// Props
interface MyComponentProps {
	headerStyle: CSSProperties;
}

<MyComponent headerStyle={{ color: 'red' }} />

Typez les événements

React fournit ses propres types d’événements, différents de ceux du DOM natif :

  • MouseEvent<HTMLButtonElement>

  • ChangeEvent<HTMLInputElement>

  • FormEvent<HTMLFormElement>

  • etc.

Ils s’utilisent comme type du paramètre dans vos handlers :

import type { MouseEvent } from 'react';

const handleClick = (e: MouseEvent<HTMLButtonElement>) => {
  console.log(e.currentTarget);
};

<button onClick={handleClick}>Click</button>

Typez les hooks de React

La plupart des hooks de React peuvent s’utiliser avec TypeScript de la même manière qu’en JavaScript traditionnel.

J’aimerais cependant vous faire part de quelques subtilités pour deux hooks principaux :useStateetuseRef. Regardons ça ensemble !

useState

Avec TypeScript,useStateest un générique : il est possible de lui donner le type attendu par l’état concerné.

const [count, setCount] = useState<number>(0);

Sachez que TypeScript peut déduire par lui-même le type de l’état en se basant par sa valeur par défaut. Ainsi, l’exemple ci-dessus et le code ci-dessous sont strictement équivalents :

const [count, setCount] = useState(0);

Et oui, c’est la même chose qu’en JavaScript, en fin de compte !

L’utilisation du générique est utile principalement si la valeur par défaut n’est pas suffisante pour que TypeScript puisse en déduire le type attendu.

C’est le cas par exemple pour les énumérations de valeurs :

type Status = 'idle' | 'loading' | 'success' | 'error';
const [status, setStatus] = useState<Status>('idle');

// ✅ Correct
setStatus('success');

// ❌ Erreur détectée par TS: 'pending' n'est pas un Status valide
setStatus('pending');

Il est également nécessaire d’expliciter le type de l’état quand la valeur par défaut est null (ou autre valeur “falsy” similaire) :

type User = { id: number; name: string };
const [currentUser, setCurrentUser] = useState<User | null>(null);

Notez dans ce dernier exemple l’utilisation de| null, qui est nécessaire.

En effet, currentUserest destiné à contenir un User, mais puisqu’il est null par défaut il est obligatoire de le préciser !

useRef

Tout comme useState, il est possible avec TypeScript de définir le type de la variable stockée en référence. Dans l’exemple ci-dessous, on s’attend donc explicitement à avoir un bouton HTML.

const buttonRef = useRef<HTMLButtonElement | null>(null);

Notez là aussi la petite astuce du| null

Grâce à cette déclaration, TypeScript saura que buttonRef.currentsera soitnull, soit une référence à un bouton HTML venant du DOM, ce qui lui permettra de vous aiguiller efficacement.

Bonus : typez les composants définis en tant queclass

Si vous souhaitez utiliser TypeScript dans un vieux projet React, il est possible que vous rencontriez l’ancienne façon de déclarer des composants, avec le mot-cléclass.

Même si utiliser class pour définir un composant est aujourd’hui une technique déconseillée, elle est toujours fonctionnelle et il peut donc être utilise de savoir la typer.

La seule chose à savoir est que la classe Component de React est un générique qui prend en paramètre les types définissants les props et le state du composant :

import { Component } from 'react';

// Type des props
export interface CounterProps {
  start?: number;
}

// Type du state
interface CounterState {
  count: number;
}

class Counter extends Component<CounterProps, CounterState> {
  static defaultProps: CounterProps = {
    start: 0
  };

  state: CounterState = {
    count: this.props.start ?? 0
  };

  increment = () => {
    this.setState((prevState) => ({
      count: prevState.count + 1
    }));
  };

  render() {
    return (
      <div>
        <p>Compteur : {this.state.count}</p>
        <button onClick={this.increment}>+1</button>
      </div>
    );
  }
}

export default Counter;

En résumé

  • Pour utiliser TypeScript dans votre projet React, installez @types/react et @types/react-dom. Configurez votre tsconfig.json pour qu’il gère le JSX (via les propriétés lib et jsx). Renommez alors vos fichiers de composants de  .jsx  en  .tsx.

  • Typez vos composants comme si vous typiez de simples fonctions. Privilégiez la déclaration via interface plutôt que via type.

  • Utilisez les types fournis par React (comme ReactNode ou ElementType) pour définir les besoins de vos composants le plus précisément possible.

  • Gérez des combinaisons complexes de props via des unions discriminantes.

  • N’utilisez pas React.FC.

  • Les hooks fournis par React (comme useState et useRef) peuvent aussi être typés : n’hésitez pas à le faire pour rendre votre code le plus robuste possible !

Ever considered an OpenClassrooms diploma?
  • Up to 100% of your training program funded
  • Flexible start date
  • Career-focused projects
  • Individual mentoring
Find the training program and funding option that suits you best