Utiliser TypeScript

TypeScript est une solution populaire d’ajout de définitions de types à des bases de code JavaScript. TypeScript prend nativement en charge JSX, et pour obtenir une prise en charge complète de la version web de React, il vous suffit d’ajouter @types/react et @types/react-dom à votre projet.

Installation

Tous les frameworks React de qualité reconnue prennent en charge TypeScript. Suivez le guide spécifique à votre framework pour l’installation :

Ajouter TypeScript à un projet React existant

Pour installer la dernière version des définitions de types de React :

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

Vous devrez définir les options de compilation suivantes dans votre tsconfig.json :

  1. dom doit figurer dans lib (Notez que si aucune option lib n’est précisée, dom est inclus par défaut).
  2. jsx doit être défini avec une valeur valide. La plupart des applications utiliseront sans doute preserve. Si vous travaillez sur une bibliothèque, consultez la documentation de jsx pour savoir quelle valeur choisir.

TypeScript pour les composants React

Remarque

Tout fichier contenant du JSX doit utiliser l’extension de fichier .tsx. Il s’agit d’une extension spécifique à TypeScript qui lui indique que le fichier contient du JSX.

Écrire du code React en TypeScript est très similaire à son écriture en JavaScript. La différence principale lorsque vous travaillez sur un composant tient à ce que vous pouvez fournir les types de ses propriétés. Ces types peuvent être utilisés pour vérifier une utilisation correcte et pour fournir une documentation à la volée dans les éditeurs.

Si on reprend le composant MyButton du guide de démarrage rapide, nous pouvons ajouter un type qui décrit le title du bouton :

function MyButton({ title }: { title: string }) {
  return (
    <button>{title}</button>
  );
}

export default function MyApp() {
  return (
    <div>
      <h1>Bienvenue dans mon appli</h1>
      <MyButton title="Je suis un bouton" />
    </div>
  );
}

Remarque

Les bacs à sable de cette documentation comprennent le code TypeScript, mais n’exécutent pas la vérification de types. Ça signifie que vous pouvez modifier les bacs à sable TypeScript pour apprendre, mais vous ne verrez aucune erreur ni aucun avertissement liés au typage. Pour bénéficier de la vérification de types, vous pouvez utiliser le TypeScript Playground ou un bac à sable en ligne aux fonctionnalités plus riches.

Cette syntaxe en ligne est la façon la plus rapide de fournir des types pour un composant, mais dès que vous commencer à avoir un certain nombre de props à décrire, elle devient difficile à lire. Utilisez plutôt une interface ou un type pour décrire les props du composant :

interface MyButtonProps {
  /** Le texte à afficher dans le bouton */
  title: string;
  /** Indique si on peut interagir avec le bouton */
  disabled: boolean;
}

function MyButton({ title, disabled }: MyButtonProps) {
  return (
    <button disabled={disabled}>{title}</button>
  );
}

export default function MyApp() {
  return (
    <div>
      <h1>Bienvenue dans mon appli</h1>
      <MyButton title="Je suis un bouton inactif" disabled={true}/>
    </div>
  );
}

Le type qui décrit les props de votre composant peut être aussi simple ou complexe que nécessaire, mais ce sera toujours un type objet utilisant soit type soit interface. Vous pouvez apprendre à décrire des objets en TypeScript avec les types objets, vous trouverez sûrement les types unions pratiques pour décrire des propriétés pouvant avoir plusieurs types, et le guide Créer des types à partir d’autres types vous aidera pour les cas plus avancés. (Tous ces liens pointent vers la documentation TypeScript qui n’a pas de version française, NdT)

Exemples de typage avec les Hooks

Les définitions de types dans @types/react incluent le typage des Hooks fournis par React, que vous pouvez donc utiliser sans configuration supplémentaire. Ces types sont conçus pour s’appuyer sur le code que vous écrivez, vous bénéficierez donc la plupart du temps de l’inférence de type, de sorte que vous ne devriez pas avoir à leur fournir des types sur-mesure.

Ceci étant dit, voyons quelques exemples de fourniture explicite de types à des Hooks.

useState

Le Hook useState réutilise la valeur initiale que vous lui passez pour déterminer le type attendu pour la variable d’état. Par exemple, le code suivant :

// Infère le type "boolean"
const [enabled, setEnabled] = useState(false);

…attribuera le type boolean à enabled, et setEnabled sera une fonction acceptant soit un argument boolean, soit une fonction de mise à jour qui accepte et renvoie un boolean. Si vous souhaitez typer l’état explicitement, vous pouvez passer un paramètre de type à l’appel useState :

// Typage explicite à "boolean"
const [enabled, setEnabled] = useState<boolean>(false);

Dans ce cas précis ça n’avait guère d’intérêt, mais pour une union par exemple, vous aurez besoin d’un typage explicite. Par exemple, le status ci-dessous a un jeu de valeurs restreint :

type Status = "idle" | "loading" | "success" | "error";

const [status, setStatus] = useState<Status>("idle");

Ou alors, comme conseillé dans Principes de structuration d’état, vous pouvez grouper des éléments d’état étroitement liés dans un objet et en décrire les différentes configurations via une union discriminante :

type RequestState =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success', data: any }
| { status: 'error', error: Error };

const [requestState, setRequestState] = useState<RequestState>({ status: 'idle' });

useReducer

Le Hook useReducer est un Hook plus complexe qui prend une fonction de réduction et un état initial. Les types de la fonction de réduction sont inférés sur base de l’état initial. Vous pouvez choisir de spécifier un paramètre de type à l’appel useReducer pour typer cet état, mais il est généralement préférable de typer l’état initial directement :

import {useReducer} from 'react';

interface State {
   count: number
};

type CounterAction =
  | { type: "reset" }
  | { type: "setCount"; value: State["count"] }

const initialState: State = { count: 0 };

function stateReducer(state: State, action: CounterAction): State {
  switch (action.type) {
    case "reset":
      return initialState;
    case "setCount":
      return { ...state, count: action.value };
    default:
      throw new Error("Unknown action");
  }
}

export default function App() {
  const [state, dispatch] = useReducer(stateReducer, initialState);

  const addFive = () => dispatch({ type: "setCount", value: state.count + 5 });
  const reset = () => dispatch({ type: "reset" });

  return (
    <div>
      <h1>Bienvenue dans mon compteur</h1>

      <p>Compteur : {state.count}</p>
      <button onClick={addFive}>Ajouter 5</button>
      <button onClick={reset}>Réinitialiser</button>
    </div>
  );
}

Nous utilisons ici TypeScript à certains endroits stratégiques :

  • interface State décrit la forme de l’état pour notre réducteur.
  • type CounterAction décrit les différentes actions susceptibles d’être dispatchées auprès du réducteur.
  • const initialState: State fournit un type pour l’état initial, qui est aussi le type qu’utilisera useReducer par défaut.
  • stateReducer(state: State, action: CounterAction): State définit les types des arguments et de la valeur de retour pour la fonction de réduction.

Pour un style plus explicite, vous pouvez plutôt définir le type d’initialState en passant un paramètre de type à useReducer :

import { stateReducer, State } from './your-reducer-implementation';

const initialState = { count: 0 };

export default function App() {
const [state, dispatch] = useReducer<State>(stateReducer, initialState);
}

useContext

Le Hook useContext permet de diffuser des données à travers l’arbre de composants sans avoir à les faire percoler explicitement via chaque niveau intermédiaire. On l’utilise pour créer un composant fournisseur, en définissant le plus souvent un Hook dédié pour en consommer la valeur dans un composant descendant.

Le type de la valeur fournie par le contexte est inféré à partir de la valeur passée à l’appel createContext :

import { createContext, useContext, useState } from 'react';

type Theme = "light" | "dark" | "system";
const ThemeContext = createContext<Theme>("system");

const useGetTheme = () => useContext(ThemeContext);

export default function MyApp() {
  const [theme, setTheme] = useState<Theme>('light');

  return (
    <ThemeContext.Provider value={theme}>
      <MyComponent />
    </ThemeContext.Provider>
  )
}

function MyComponent() {
  const theme = useGetTheme();

  return (
    <div>
      <p>Thème actif : {theme}</p>
    </div>
  )
}

Cette technique fonctionne lorsque vous avez une valeur par défaut pertinente — mais il arrive que ça ne soit pas le cas, et que vous utilisiez alors null comme valeur par défaut. Le souci, c’est que pour satisfaire le système de typage, vous allez devoir explicitement passer à createContext un paramètre de type ContextShape | null.

Ça va complexifier votre code en vous forçant à éliminer le | null du type pour les consommateurs du contexte. Nous vous conseillons d’incorporer un type guard au sein de votre Hook personnalisé pour vérifier que la valeur existe bien, et lever une exception dans le cas contraire :

import { createContext, useContext, useState, useMemo } from 'react';

// C'est un exemple simplifié, imaginez quelque chose de plus riche
type ComplexObject = {
kind: string
};

// Le contexte est créé avec `| null` dans son type, pour autoriser
// la valeur par défaut.
const Context = createContext<ComplexObject | null>(null);

// Le `| null` sera retiré grâce à une vérification au sein du Hook.
const useGetComplexObject = () => {
const object = useContext(Context);
if (!object) { throw new Error("useGetComplexObject must be used within a Provider") }
return object;
}

export default function MyApp() {
const object = useMemo(() => ({ kind: "complex" }), []);

return (
<Context.Provider value={object}>
<MyComponent />
</Context.Provider>
)
}

function MyComponent() {
const object = useGetComplexObject();

return (
<div>
<p>Objet courant : {object.kind}</p>
</div>
)
}

useMemo

Le Hook useMemo mémoïse les valeurs renvoyées par une fonction, pour ne re-exécuter celle-ci que si les dépendances passées en deuxième paramètre ont changé. Le type du résultat de l’appel au Hook est inféré sur base de la valeur de retour de la fonction passée en premier argument. Vous pouvez choisir de passer un paramètre de type explicitement.

// Le type de `visibleTodos` est inféré à partir du type du résultat
// de `filterTodos`
const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);

useCallback

Le Hook useCallback fournit une référence stable à une fonction tant que les dépendances passées en deuxième argument ne changent pas. De façon similaire à useMemo, le type de la fonction est inféré sur base du type de la fonction passée en premier argument, et vous pouvez passer un paramètre de type explicite si vous le souhaitez.

const handleClick = useCallback(() => {
// ...
}, [todos]);

Lorsque vous utilisez le mode strict de TypeScript, useCallback exigera le typage détaillé de la fonction que vous lui passez, notamment pour ses arguments. Selon vos préférences stylistiques, vous pourrez le faire soit avec un typage classique de signature, soit avec un paramètre de type passé au Hook, en exploitant les fonctions *EventHandler fournies par les définitions de types de React, comme ceci :

import { useState, useCallback } from 'react';

export default function Form() {
const [value, setValue] = useState("Change me");

const handleChange = useCallback<React.ChangeEventHandler<HTMLInputElement>>((event) => {
setValue(event.currentTarget.value);
}, [setValue])

return (
<>
<input value={value} onChange={handleChange} />
<p>Valeur : {value}</p>
</>
);
}

Types utiles

Le module @types/react fournit un vaste ensemble de types ; une fois que vous serez à l’aise avec l’utilisation combinée de React et TypeScript, ça vaut le coup d’explorer son contenu. Vous le trouverez dans le dossier de React sur DefinitelyTyped. Nous allons passer ici en revue les types les plus courants.

Événements DOM

Lorsque vous travaillez avec des événements DOM en React, le type de l’événement peut souvent être inféré sur base du gestionnaire d’événement. Cependant, si vous souhaitez extraire la fonction qui sera passée comme gestionnaire, vous devrez typer l’événement explicitement.

import { useState } from 'react';

export default function Form() {
  const [value, setValue] = useState("Modifiez-moi");

  function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
    setValue(event.currentTarget.value);
  }

  return (
    <>
      <input value={value} onChange={handleChange} />
      <p>Valeur : {value}</p>
    </>
  );
}

Les types React fournissent de nombreux types d’événements : la liste complète est ici, elle reprend la majorité des événements courants du DOM.

Pour déterminer le type dont vous avez besoin, vous pouvez commencer par regarder l’infobulle au survol du gestionnaire que vous utilisez : elle affichera le type de l’événement.

Si vous avez besoin d’un type d’événement qui ne figure pas dans la liste, vous pouvez utiliser le type React.SyntheticEvent, qui est le type de base pour tous les autres.

Enfants

Il y a deux façons courantes de décrire les enfants d’un composant. La première consiste à utiliser le type React.ReactNode, qui est une union de tous les types d’enfants possibles dans JSX :

interface ModalRendererProps {
title: string;
children: React.ReactNode;
}

C’est là une définition très large pour les enfants. La seconde utilise plutôt le type React.ReactElement, qui ne permet que les éléments JSX et pas les nœuds primitifs tels que les chaînes de caractères ou les nombres :

interface ModalRendererProps {
title: string;
children: React.ReactElement;
}

Notez que vous ne pouvez pas utiliser TypeScript pour retreindre le type de vos enfants à certains éléments JSX spécifiques, vous ne pouvez donc pas vous appuyer sur le système de typage pour indiquer qu’un composant n’accepterait par exemple que des enfants <li>.

Vous trouverez un exemple complet avec React.ReactNode et React.ReactElement et la vérification de types activée dans ce bac à sable TypeScript.

Props de style

Lorsque vous utilisez des styles en ligne dans React, vous pouvez utiliser React.CSSProperties pour typer l’objet passé à la prop style. Ce type regroupe toutes les propriétés CSS possibles, c’est une bonne façon de vous assurer que vous ne passez que de propriétés CSS valides à votre prop style, et d’obtenir une complétion automatique dans votre éditeur.

interface MyComponentProps {
style: React.CSSProperties;
}

Aller plus loin

Ce guide a couvert les bases de l’utilisation de TypeScript avec React, mais il reste beaucoup à apprendre. Les pages dédiées de la documentation pour chaque API fournissent davantage d’information sur leur utilisation avec TypeScript.

Nous vous conseillons les ressources suivantes (toutes en anglais, NdT) :