Comment utiliser Typescript dans une application React ?

Le 14 décembre 2021 - Temps de lecture 16 minutes

Comment utiliser Typescript dans une application React ?

Illustration Typescript et React

Comment typer les props React avec Typescript ?

Puisque les props React sont utilisés pour transmettre des données entre deux composants React, il existe de nombreux types que vous pouvez utiliser pour typer les props React.
Pour écrire les types de vos props, vous devez ajouter deux points et la notation littérale d'objet ( : {}) à côté de l'assignation de déstructuration de la prop enfant dans la déclaration du composant. Voici un exemple de saisie d'une chaîne de caractères et d'un nombre :

const App = ({ title, score }: { title: string; score: number }) => (
  <h1>
    {title} = {score}
  </h1>
);

Crééer un alias pour le type des props

Comme la convention dans React est d'écrire un composant dans un fichier .js ou .jsx, vous pouvez déclarer un alias de type pour les props du composant afin de rendre le code plus facile à lire. Voici un exemple de création d'un alias de type pour les props du composant App :

type Props = {
  title: string;
  score: number;
};

const App = ({ title, score }: Props) => (
  <h1>
    {title} = {score}
  </h1>
);

Comment typer les props optionnelles ?

Vous pouvez rendre une option facultative en ajoutant le symbole du point d'interrogation ? après le nom de l'option. L'exemple suivant rend la props titre facultatif :

type Props = {
  title?: string;
  score: number;
};

La prop optionnelle signifie que vous pouvez rendre le composant sans passer la prop, mais lorsque vous passez la prop, elle doit être du type déclaré.

Liste des types pour les props des composants

Maintenant que vous savez comment vérifier le type des props, voici une liste des types courants que vous pouvez vouloir utiliser dans votre application React. Tout d'abord, vous avez des types primitifs comme les chaînes de caractères, les nombres et les booléens, comme indiqué ci-dessous :

type Props = {
  // primitive types
  title: string;
  score: number;
  isWinning: boolean;
};

Vous pouvez également créer un tableau d'un seul type en ajoutant la notation littérale de tableau ([]) après le type, comme suit :

type Props = {
  title: string[]; // an array of string
  score: number;
  isWinning: boolean;
};

Vous pouvez également écrire des valeurs littérales pour spécifier les valeurs exactes qui peuvent être acceptées par la props.
Vous devez séparer les valeurs littérales à l'aide de l'opérateur pipe simple | comme indiqué ci-dessous :

type Props = {
  priority: 'high' | 'normal' | 'low';
  score: 5 | 9 | 10;
};

TypeScript lancera une erreur statique si la valeur de la priorité ou du score prop ci-dessus ne correspond pas à l'une des valeurs littérales.
Ensuite, vous pouvez typer un objet prop comme suit :

type Props = {
  user: {
    username: string;
    age: number;
    isMember: boolean;
  };
};

Lorsque vous avez un tableau d'objets prop, il suffit d'ajouter la notation array literal à la fin de la déclaration de l'objet comme suit :

type Props = {
  user: {
    username: string;
    age: number;
    isMember: boolean;
  }[]; // ICI
};

Les props React peuvent également recevoir des fonctions telles que onClick et onChange, vous pouvez donc avoir besoin de typer ces paramètres.
Vous pouvez typer les paramètres acceptés par la fonction ou prendre un objet événement du HTML comme indiqué ci-dessous :

type Props = {
  // Fonction qui ne retourne rien.
  onClick: () => void;
  // Fonction qui prend un paramètre chaîne et retourne un booléen.
  onChange: (target: string) => boolean;
  // Fonction qui prend un évènement en paramètre.
  handleSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
};

Si vous déclarez une fonction onChange dans le corps du composant, vous pouvez immédiatement vérifier le paramètre et le type de retour de la fonction, comme indiqué ci-dessous :

const App = () => {
  const [message, setMessage] = useState('');

  const onChange = (e: React.FormEvent<HTMLInputElement>): void => {
    setMessage(e.currentTarget.value);
  };

  // code supprimé pour alléger...
};

Enfin, les composants React peuvent accepter un autre composant comme prop d'enfants, vous devez donc utiliser ReactNode pour saisir ces props d'enfants :

type Props = {
  children: React.ReactNode;
};

const App = ({ children }: Props) => <div>{children}</div>;

Comment typer les composants fonctionnels ?

Vous pouvez combiner le type Props et le type React.FC pour créer un composant bien typé.

type Props = {
  title: string;
};

const App: React.FC<Props> = ({ title }) => {
  return <h1>{title}</h1>;
};

Lorsque vous appelez le composant App ci-dessus, vous devrez spécifier la prop message avec le type string. Mais puisque TypeScript est capable de déduire le type de votre variable, vous pouvez supprimer le typage du composant avec React.FC comme ceci :

const App = ({ title }: Props) => <div>{title}</div>;
// Le type est déduit

Si vous n'avez que quelques props pour votre composant vous pouvez directement le typer comme ceci :

const App = ({ title }: { title: string }) => <div>{title}</div>;

Grâce à la fonction de type inféré de TypeScript, vous n'avez pas besoin de typer les composants fonctionnels.

Comment typer les Hooks React ?

Les hooks React sont supportés par la bibliothèque @types/react à partir de la version 16.8. En général, Typescript devrait être capable de déduire le type de vos hooks, à moins que vous n'ayez des cas spécifiques où le type doit être déclaré explicitement. Voyons comment typer les hooks React un par un, en commençant par le hook useState.

Comment typer useState ?

La valeur de useState peut être déduite de la valeur initiale que vous avez définie lorsque vous appelez la fonction.

Par exemple, l'appel useState() suivant initialise l'état avec une chaîne vide. Lorsque vous appelez la fonction setState, vous devez mettre une chaîne de caractères, sinon il y aura une erreur :

const App = () => {
  const [title, setTitle] = useState(''); // type string
  const changeTitle = () => {
    setTitle(9); // Erreur: number n'est pas assignable à string!
  };
};

Mais lorsque vous devez initialiser votre état avec des valeurs telles que null ou undefined, vous devez ajouter un générique lorsque vous initialisez l'état. Un générique vous permet d'utiliser plusieurs types pour le hook useState comme indiqué ci-dessous :

// title is string or null
const [title, setTitle] = useState<string | null>(null);
// score is number or undefined
const [score, setScore] = useState<number | undefined>(undefined);

Lorsque vous avez un objet complexe comme valeur d'état, vous pouvez créer une interface ou un type pour cet objet comme suit :

interface Member {
  username: string;
  age?: number;
}
const [member, setMember] = useState<Member | null>(null);

Et c'est ainsi que vous pouvez typer les hooks useState dans votre application.

Comment typer les hooks useEffect et useLayoutEffect

Vous n'avez pas besoin de typer les hooks useEffect et useLayoutEffect car ils ne traitent pas de valeurs de retour. La fonction de nettoyage du hook useEffect n'est pas non plus considérée comme une valeur qui peut être modifiée. Vous pouvez écrire ces hooks normalement.

Comment typer le hook useContext

Le type du hook useContext est généralement déduit de la valeur initiale que vous avez passée à la fonction createContext() comme suit :

const AppContext = createContext({
  authenticated: true,
  lang: 'en',
  theme: 'dark',
});
const MyComponent = () => {
  const appContext = useContext(AppContext); //inferred as an object
  return <h1>The current app language is {appContext.lang}</h1>;
};

La valeur contextuelle ci-dessus sera déduite comme l'objet suivant :

{
  authenticated: boolean,
  lang: string,
  theme: string
}

Vous pouvez également créer un type qui servira de générique à la valeur de retour de CreateContext.
Par exemple, supposons que vous ayez un ThemeContext qui ne possède que deux valeurs : light et dark.
Voici comment vous typer le contexte :

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

Le type sera utilisé lorsque vous définirez la valeur du contexte à l'aide de ThemeContext.Provider plus tard dans votre code.

Ensuite, le hook useContext déduira le type à partir de l'objet contextuel ThemeContext que vous avez passé en argument :

const App = () => {
  const theme = useContext(ThemeContext);
  return <div>The theme is {theme}</div>;
};

Comment typer le hook useRef ?

D'après la documentation de React, le hook useRef est généralement utilisé pour faire référence à un élément HTML comme ici :

function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current` points sur l'élément monté
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Mettre le focus sur l'input</button>
    </>
  );
}

En suivant ce cas d'utilisation, vous pouvez écrire un générique qui accepte HTMLInputElement comme indiqué ci-dessous :

const inputRef = useRef<HTMLInputElement>(null);

Comment typer le hook useMemo ?

Le hook useMemo retourne une valeur mémorisée, donc le type sera déduit de la valeur retournée :

const num = 24;
// déduit comme une nombre à partir de la valeur retournée ci-dessous
const result = useMemo(() => Math.pow(10, num), [num]);

Comment typer le hook useCallback ?

Le hook useCallback renvoie une fonction de rappel mémorisée, le type sera donc déduit de la valeur renvoyée par la fonction de rappel :

const num = 9;
const callbackFn = useCallback(
  (num: number) => {
    return num * 2; // type déduit number
  },
  [num]
);

Comment typer les hooks personnalisés ?

Comme les hooks personnalisés sont des fonctions, vous pouvez ajouter des types explicites pour leurs paramètres tout en déduisant leur type à partir de la valeur retournée.

function useFriendStatus(friendID: number) {
  const [isOnline, setIsOnline] = useState(false);
  // code pour changer l'état isOnline omis
  return isOnline;
}
const status = useFriendStatus(9); // type inferré boolean

Lorsque vous retournez un tableau comme dans le cas du hook useState, vous devez affirmer que la valeur retournée est const afin que TypeScript ne déduise pas votre type comme une union

function useCustomHook() {
  return ['Hello', false] as const;
}

Sans l'assertion as const, TypeScript déduira les valeurs retournées comme (string | boolean)[] au lieu de [string, boolean]. Et c'est ainsi que vous pouvez saisir les hooks React. Nous allons maintenant apprendre à saisir les événements et les formulaires HTML.

Comment saisir les événements et les formulaires HTML

La plupart des types d'événements HTML peuvent être déduits correctement par TypeScript, vous n'avez donc pas besoin de définir explicitement le type.

Par exemple, l'événement onClick d'un élément bouton sera déduit comme React.MouseEvent par TypeScript :

const App = () => (
  <button onClick={(event) => console.log('Clicked')}>button</button>
  // event inférré comme React.MouseEvent<HTMLButtonElement, MouseEvent>
);

Pour les formulaires HTML, vous devrez saisir l'événement onSubmit comme React.FormEvent car l'inférence par défaut Any lancera une erreur.

Mais les événements onChange pour vos entrées HTML peuvent généralement être déduits de l'événement lui-même.

Voici un exemple de formulaire React en TypeScript :

const App = () => {
  const [email, setEmail] = useState('');

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    // handle submission here...
    alert(`email value: ${email}`);
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label>
          Email:
          <input
            type="email"
            name="email"
            onChange={(e) => setEmail(e.currentTarget.value)}
            // ^^^ onChange inferred as React.ChangeEvent
          />
        </label>
      </div>
      <div>
        <input type="Submit" value="Submit" />
      </div>
    </form>
  );
};

Comprendre la différence entre ReactElement JSX.Element et ReactNode

Bien que TypeScript puisse déduire le type de retour des composants de fonction React au fur et à mesure que vous codez les composants, vous pouvez avoir un projet avec une règle de linting qui exige que le type de retour soit explicitement défini.

La bibliothèque @types/react possède plusieurs types que vous pouvez utiliser pour définir le type de retour des composants de fonction React. Il s'agit de :

  • ReactElement
  • JSX.Element
  • ReactNode

Cette section est dédiée à vous aider à comprendre ces types et quand les utiliser.

ReactElement

Un ReactElement est une interface pour un objet avec des propriétés type, props et key comme indiqué ci-dessous :

type Key = string | number;
interface ReactElement<
  P = any,
  T extends string | JSXElementConstructor<any> =
    | string
    | JSXElementConstructor<any>
> {
  type: T;
  props: P;
  key: Key | null;
}

JSXElement

Un JSX.Element est une extension de ReactElement qui a le type<T> et les props<P> implémentés comme tout comme vous pouvez le voir dans le dépôt :

declare global {
  namespace JSX {
    interface Element extends React.ReactElement<any, any> {}
  }
}

Le type de ReactElement est plus strict que celui de JSX.Element, mais ils sont essentiellement les mêmes.

ReactNode

Enfin, ReactNode est un type très souple car il inclut tout ce qui peut être retourné par la méthode render() des composants de la classe React.

Dans le dépôt, ReactNode est défini comme suit :

type ReactNode =
  | ReactChild
  | ReactFragment
  | ReactPortal
  | boolean
  | null
  | undefined;

C'est pourquoi, lorsque votre composant a une prop enfant qui peut recevoir un autre composant, il est recommandé d'utiliser ReactNode comme type car il peut recevoir tout ce qui peut être rendu par React.

D'autre part, ReactElement et JSX.Element sont plus stricts comparés à ReactNode car ils ne permettent pas de retourner des valeurs comme null.

Quand utiliser ReactElement JSX.Element ou ReactNode

Le type ReactNode est mieux utilisé pour typer une prop enfant qui peut recevoir un autre composant React ou des éléments JSX comme ceci :

const App = ({ children }: { children: React.ReactNode }) => {
  return <div>{children}</div>;
};
// At index.tsx
<App>
  <Header />
  <h2>Another title</h2>
</App>;

En effet, les types ReactElement et JSX.Element sont plus stricts sur le type de retour (ils n'autorisent pas les nullités) et ils s'attendent à ce que vous retourniez un seul élément.

Pour accepter des enfants uniques et multiples pour ces deux types, vous devez utiliser ReactElement | ReactElement[] ou JSX.Element | JSX.Element[] comme type d'enfant.

Les types ReactElement et JSX.Element sont plus adaptés pour définir explicitement le type de retour d'un composant React, comme ici :

const App = (): React.ReactElement | JSX.Element => {
  return <div>hello</div>;
};

Mais puisque nous parlons ici de meilleures pratiques, je vous recommande de suivre la définition de l'interface FunctionComponent dans la bibliothèque des types, qui utilise ReactElement<any, any> | null :

interface FunctionComponent<P = {}> {
  (props: PropsWithChildren<P>, context?: any): ReactElement<any, any> | null;
  propTypes?: WeakValidationMap<P> | undefined;
  contextTypes?: ValidationMap<any> | undefined;
  defaultProps?: Partial<P> | undefined;
  displayName?: string | undefined;
}

Et parce que JSX.Element est exactement l'extension de ReactElement<any, any>, vous pouvez définir un type de retour de composant de fonction React comme suit :

const App = (): JSX.Element | null => {
  return <div>hello</div>;
};

De cette façon, votre composant peut toujours ne rien rendre en retournant null.
J'espère que cette section vous a aidé à comprendre les différents types qui peuvent être utilisés pour le typage des composants React.

Comment typer (étendre) les éléments HTML

Parfois, vous souhaitez créer un petit composant modulaire qui prend les attributs d'un élément HTML natif comme prop.
Les éléments button, img, ou input sont des composants utiles que vous pouvez créer pour votre application.

type ButtonProps = {
  children: React.ReactNode;
  onClick: () => void;
};
const Button = ({ children, onClick }: ButtonProps) => {
  return <button onClick={onClick}>{children}</button>;
};

la bibliothèque @types/react embarque le type ComponentPropsWithoutRef qui peut être utilisé pour embarquer tous les attributs natifs d'un élément HTML dans votre nouveau composant.
Par example, l'élément natif button connait déjà l'attribut onClick, mais si vous créez un composant <Button> vous devrez passer les props natives individuellement une à une. Cela peut-être long et fastidieux, et surtout source d'oubli.

type ButtonProps = {
  children: React.ReactNode;
  onClick: () => void;
};

const Button = ({ children, onClick }: ButtonProps) => {
  return <button onClick={onClick}>{children}</button>;
};

Avec l'exemple ci-dessus, vous devez continuer à ajouter une autre prop aux ButtonProps au fur et à mesure que vous en avez besoin, comme suit :

type ButtonProps = {
  children: React.ReactNode;
  onClick: () => void;
  disabled: boolean;
  type: 'button' | 'submit' | 'reset' | undefined;
};

Le type ComponentPropsWithoutRef peut être utilisé pour que vous n'ayez pas à ajouter ces attributs HTML natifs au type au fur et à mesure de l'évolution de votre application. Vous pouvez simplement créer un type qui a tous les attributs de bouton natifs comme props comme ceci :

type ButtonProps = React.ComponentPropsWithoutRef<'button'>;
const Button = ({ children, onClick, type }: ButtonProps) => {
  return (
    <button onClick={onClick} type={type}>
      {children}
    </button>
  );
};

Le type ComponentPropsWithoutRef<"button"> a toutes les props de l'élément button HTML natif.

Si vous souhaitez créer un composant <Img>, vous pouvez donc utiliser le type ComponentPropsWithoutRef<"img">

type ImgProps = React.ComponentPropsWithoutRef<'img'>;
const Img = ({ src, loading }: ImgProps) => {
  return <img src={src} loading={loading} />;
};

Lorsque vous devez ajouter une propriété personnalisée qui n'existe pas dans l'élément HTML natif, vous pouvez créer une interface qui étend les attributs natifs comme suit :

interface ImgProps extends React.ComponentPropsWithoutRef<'img'> {
  customProp: string;
}
const Img = ({ src, loading, customProp }: ImgProps) => {
  // use the customProp here..
  return <img src={src} loading={loading} />;
};

Le type ComponentPropsWithoutRef permet de créer facilement un composant qui est une extension des éléments HTML natifs sans avoir à saisir vous-même tous les paramètres prop possibles.

L'interface ComponentPropsWithoutRef possède également un jumeau appelé ComponentPropsWithRef que vous pouvez utiliser lorsque vous devez transmettre une référence aux enfants du composant.

ComponentPropsWithoutRef vs [Element]HTMLAttributes

Si vous avez déjà utilisé TypeScript avec React, vous connaissez peut-être l'interface [Element]HTMLAttributes de la bibliothèque @types/react que vous pouvez utiliser pour étendre les éléments HTML comme suit :

type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement>;
type ImgProps = React.ImgHTMLAttributes<HTMLImageElement>;

Ces interfaces [Element]HTMLAttributes produisent le même type que l'interface ComponentPropsWithoutRef, mais elles sont plus verbeuses puisque vous devez utiliser une interface et un générique différents pour chaque élément HTML.

D'un autre côté, ComponentPropsWithoutRef ne vous demande que de modifier le type générique <T>. Les deux sont parfaits pour étendre les éléments HTML dans les composants React.

Type vs Interface - Comment choisir ?

Les types et les interfaces de TypeScript peuvent être utilisés pour définir les props, les composants et les hooks de React.

La documentation nous dit ceci :

Type aliases and interfaces are very similar, and in many cases you can choose between them freely. Almost all features of an interface are available in type, the key distinction is that a type cannot be re-opened to add new properties vs an interface which is always extendable.

Les alias de type et les interfaces sont très similaires, et dans de nombreux cas, vous pouvez choisir librement entre les deux. Presque toutes les fonctionnalités d'une interface sont disponibles dans un type, la principale distinction étant qu'un type ne peut pas être rouvert pour ajouter de nouvelles propriétés, contrairement à une interface qui est toujours extensible.

Lorsque vous utilisez des interfaces, vous pouvez librement étendre une interface comme suit :

interface HtmlAttributes {
  disabled: boolean;
}
interface ButtonHtmlAttributes extends HtmlAttributes {
  type: 'Submit' | 'Button' | null;
}

Mais les types ne peuvent pas être étendus comme les interfaces. Vous devez utiliser le symbole d'intersection (&) comme suit :

type HtmlAttributes = {
  disabled: boolean;
};

type ButtonHtmlAttributes = HtmlAttributes & {
  type: 'Submit' | 'Button' | null;
};

Ensuite, une déclaration relative à une interface est toujours un objet, tandis qu'une déclaration de type peut être de valeurs primitives, comme indiqué ci-dessous :

type isLoading = boolean;
type Theme = 'dark' | 'light';
type Lang = 'en' | 'fr';

Aucun des exemples ci-dessus n'est possible avec une interface, de sorte qu'un type peut être préféré pour les valeurs simples d'un objet contextuel. La question est de savoir quand utiliser l'un plutôt que l'autre. Toujours d'après le manuel TypeScript :

If you would like a heuristic, use interface until you need to use features from type.

Si vous souhaitez une heuristique, utilisez l'interface jusqu'à ce que vous ayez besoin d'utiliser les caractéristiques du type.

L'analyseur de code TypeScript vous indiquera quand vous devez strictement utiliser une interface ou un type.

Si vous n'êtes pas sûr de savoir laquelle utiliser, choisissez toujours l'interface jusqu'à ce que vous voyiez une raison d'utiliser le type.

Si vous avez besoin de plus de détails, voici une réponse de StackOverflow qui compare les interfaces et les types.