Menu

React avec TypeScript : typer props, état et événements

Comment typer une application React avec TypeScript : props et children, useState avec unions et null, useRef pour les éléments DOM, gestionnaires d'événements, contexte, composants génériques et props des éléments natifs.

Cette page contient des éditeurs exécutables - modifiez, exécutez et voyez la sortie instantanément.

Utiliser React avec TypeScript, c'est écrire les composants dans des fichiers .tsx où les props, l'état, les refs et les gestionnaires d'événements ont des types, pour que le compilateur détecte une prop manquante ou un mauvais gestionnaire avant l'exécution du code. Les types disparaissent au moment du build : ce qui s'exécute dans le navigateur est le même JavaScript que vous auriez écrit sans eux. Les éditeurs en direct de cette page exécutent ce JavaScript, et la version typée figure à côté de chacun dans un bloc statique.

Voici le même Badge avec des types. count est facultatif (?), donc le second badge se rabat sur la valeur par défaut 0, et children accepte aussi bien un élément qu'une simple chaîne.

type BadgeProps = {
    label: string;
    count?: number;
    children: React.ReactNode;
};

function Badge({ label, count = 0, children }: BadgeProps) {
    return (
        <div>
            <strong>{label}</strong> ({count})
            <div>{children}</div>
        </div>
    );
}

<Badge count={3}>Hi</Badge>;
// Error: Property 'label' is missing in type '{ children: string; count: number; }'

Créer un projet TypeScript

Vite propose un modèle React plus TypeScript :

npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
npm run dev

Vous obtenez des fichiers .tsx, un tsconfig.json avec strict activé, et les définitions de types de React (@types/react, @types/react-dom) déjà installées. Vite retire les types pendant qu'il sert et construit l'application, mais ne les vérifie pas ; le script build du modèle lance d'abord tsc, et votre éditeur vérifie pendant que vous tapez. Les frameworks comme Next.js configurent TypeScript de la même façon quand vous créez un projet avec leur CLI.

Typer les props

Décrivez les props sous forme de type objet et annotez le paramètre déstructuré. Quelques patterns couvrent la plupart des composants :

interface UserCardProps {
    name: string;                       // required
    age?: number;                       // optional, may be undefined
    role: 'admin' | 'member';           // a union of allowed strings
    tags: string[];
    onSelect: (id: string) => void;     // a callback prop
    children?: React.ReactNode;         // anything React can render
}

function UserCard({ name, age, role, tags, onSelect, children }: UserCardProps) {
    // ...
}

type ou interface. Les deux fonctionnent pour les props. interface peut être étendue avec extends et fusionnée en la déclarant deux fois ; type peut aussi exprimer des unions, des intersections et des types mappés. La plupart des équipes en choisissent un et l'utilisent partout.

children. Utilisez React.ReactNode pour tout ce qui va entre les balises : éléments, chaînes, nombres, tableaux, null. La page sur les props explique comment children arrive au départ.

Pourquoi React.FC est facultatif. const Badge: React.FC<BadgeProps> = (...) => ... type toute la fonction au lieu du paramètre. Depuis les types de React 18, il n'ajoute plus children pour vous, il ne peut pas prendre de paramètre de type pour un composant générique, et il n'apporte rien qu'un paramètre typé n'apporte déjà. L'ancien code l'utilise beaucoup ; les simples fonctions avec des props typées sont le choix courant pour le nouveau code.

Typer useState

Pour une valeur initiale simple, l'inférence suffit : useState(0) est un number, useState('') une string. Passez un argument de type quand l'état peut contenir plus que ce que suggère sa valeur initiale : une valeur qui vaut d'abord null, ou l'une de plusieurs chaînes fixes.

Cliquez sur "Load user" et le statut passe de idle à loading puis à done. L'état typé :

type User = { id: number; name: string };
type Status = 'idle' | 'loading' | 'done';

const [user, setUser] = useState<User | null>(null);
const [status, setStatus] = useState<Status>('idle');

user.name;          // Error: 'user' is possibly 'null'
user?.name;         // OK
setStatus('ready'); // Error: Argument of type '"ready"' is not assignable to parameter of type 'SetStateAction<Status>'

Sans <User | null>, useState(null) déduit le type null et setUser(data) ne compile pas. L'union vous oblige à gérer le cas vide, ce qui correspond exactement à la vérification user ? ... : ... de l'exemple exécutable.

useRef pour les éléments DOM

Une ref qui pointe vers un élément DOM prend le type de l'élément et vaut d'abord null, car l'élément n'existe pas tant que React ne l'a pas attaché après le premier rendu.

Tapez quelque chose, puis cliquez sur "Log the value" : la console affiche ce que vous avez tapé, lu directement dans l'élément DOM. La version typée :

const inputRef = useRef<HTMLInputElement>(null);
// inputRef.current is HTMLInputElement | null

inputRef.current?.focus();    // OK
inputRef.current.focus();     // Error: 'inputRef.current' is possibly 'null'

const timerId = useRef<number | null>(null);  // a mutable value, not a DOM node

Utilisez l'interface d'élément qui correspond à la balise : HTMLInputElement, HTMLDivElement, HTMLCanvasElement, HTMLButtonElement. Dans les types de React 19, useRef prend toujours un argument et renvoie un RefObject dont vous pouvez affecter current, donc la même forme fonctionne pour les refs DOM et pour des valeurs comme un identifiant de minuteur. Voir useRef pour le côté exécution.

Types des gestionnaires d'événements

Les gestionnaires inline n'ont besoin d'aucune annotation : dans onChange={(e) => setName(e.target.value)}, TypeScript connaît e grâce à la prop à laquelle il est passé. Annotez quand vous déplacez un gestionnaire dans sa propre fonction.

Envoyez le formulaire et la console affiche l'adresse ; preventDefault empêche le rechargement de la page. Avec les types :

function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
    setEmail(e.target.value); // e.target is HTMLInputElement, value is string
}

function handleSubmit(e: React.SubmitEvent<HTMLFormElement>) {
    e.preventDefault();
}

function handleClick(e: React.MouseEvent<HTMLButtonElement>) {}
function handleKey(e: React.KeyboardEvent<HTMLInputElement>) {
    if (e.key === 'Enter') { /* ... */ }
}

Depuis @types/react 19.2.10, onSubmit est typé avec React.SubmitEvent et l'ancien React.FormEvent est marqué obsolète ; avec une version plus ancienne des types, écrivez plutôt React.FormEvent<HTMLFormElement>. Le paramètre de type est l'élément auquel le gestionnaire est attaché. Si vous n'êtes pas sûr du type, survolez la prop onChange dans votre éditeur, ou typez tout le gestionnaire d'un coup : const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => { ... }.

Typer le contexte

Donnez à createContext le type de la valeur. S'il existe une valeur par défaut sensée, passez-la, et chaque consommateur reçoit une valeur non nulle :

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

const theme = useContext(ThemeContext); // Theme

Quand il n'y a pas de bonne valeur par défaut (un utilisateur connecté, un store), commencez par null et enveloppez useContext dans un hook qui lève une erreur si un composant est utilisé hors du provider. Chaque appelant obtient alors le type non nul et un message d'erreur clair au lieu d'un plantage au fond du code.

type Auth = { user: string; logout: () => void };
const AuthContext = createContext<Auth | null>(null);

export function useAuth(): Auth {
    const value = useContext(AuthContext);
    if (!value) throw new Error('useAuth must be used inside <AuthProvider>');
    return value;
}

export function AuthProvider({ children }: { children: React.ReactNode }) {
    const [user, setUser] = useState('Ada');
    return (
        <AuthContext value={{ user, logout: () => setUser('') }}>
            {children}
        </AuthContext>
    );
}

<AuthContext value={...}> est la syntaxe de provider de React 19 ; <AuthContext.Provider value={...}> se type de la même façon.

Composants génériques

Une liste, un tableau ou un select qui fonctionne avec n'importe quel type d'élément est un composant générique : un paramètre de type relie la prop items au callback renderItem.

Le List typé :

type ListProps<T> = {
    items: T[];
    renderItem: (item: T) => React.ReactNode;
    getKey: (item: T) => string | number;
};

function List<T>({ items, renderItem, getKey }: ListProps<T>) {
    return <ul>{items.map((item) => <li key={getKey(item)}>{renderItem(item)}</li>)}</ul>;
}

<List items={books} getKey={(b) => b.isbn} renderItem={(b) => b.title} />;
// b is inferred as the book type, so b.titel would be an error

TypeScript déduit T à partir de items, donc les appelants n'écrivent jamais le paramètre de type. Dans un fichier .tsx, une fonction fléchée a besoin de <T,> au lieu de <T>, car <T> seul se lit comme une balise JSX.

Étendre les props des éléments natifs

Une enveloppe autour d'un button ou d'un input doit accepter tous les attributs de l'élément natif. React.ComponentProps<'button'> est cet ensemble complet ; ajoutez vos propres props par-dessus et décomposez le reste.

type ButtonProps = React.ComponentProps<'button'> & {
    variant?: 'primary' | 'ghost';
};

function Button({ variant = 'primary', style, ...rest }: ButtonProps) {
    return (
        <button
            {...rest}
            style={{ fontWeight: variant === 'primary' ? 600 : 400, ...style }}
        />
    );
}

<Button type="submit" disabled onClick={(e) => console.log(e.currentTarget)}>
    Save
</Button>;

onClick, disabled, type et aria-* sont tous typés sans les lister. Dans React 19, ref est une prop ordinaire, et ComponentProps<'button'> l'inclut, donc <Button ref={buttonRef}> atteint le nœud DOM via la décomposition sans forwardRef. Utilisez ComponentPropsWithoutRef<'button'> quand vous voulez exclure ref, et ComponentProps<typeof UserCard> pour lire les props de l'un de vos propres composants.

Erreurs de type courantes

"'ref.current' is possibly 'null'". L'élément n'existe pas pendant le premier rendu. Utilisez ref.current?.focus(), ou vérifiez if (ref.current) avant de l'utiliser.

"Type 'string' is not assignable" sur un état union. const [status, setStatus] = useState('idle') déduit string, plus large que voulu, et le passer à une prop typée Status échoue. Écrivez useState<Status>('idle').

Un événement typé avec le mauvais élément. React.ChangeEvent<HTMLInputElement> sur un select donne à e.target le mauvais type. Le paramètre de type doit correspondre à l'élément auquel le gestionnaire est attaché.

Utiliser any pour faire taire une erreur. Cela désactive la vérification pour tout ce que touche cette valeur. Préférez unknown et affinez-le, ou corrigez le type à sa source.

Questions fréquentes

Comment créer une application React avec TypeScript ?

Lancez npm create vite@latest my-app -- --template react-ts, puis npm install et npm run dev. Les composants vont dans des fichiers .tsx et Vite retire les types pendant le build ; lancez tsc (le script build du modèle le fait) pour les vérifier.

Quel est le type de children dans React ?

React.ReactNode. Il accepte tout ce que React peut afficher : éléments, chaînes, nombres, tableaux, null et undefined. N'utilisez React.ReactElement que lorsque vous avez besoin d'exactement un élément.

Comment typer useState avec null ?

Passez explicitement le type sous forme d'union : useState<User | null>(null). Sans l'argument de type, TypeScript déduit que null est la seule valeur autorisée.

Quel est le type d'un événement onChange en React ?

React.ChangeEvent<HTMLInputElement> pour un input (HTMLTextAreaElement ou HTMLSelectElement pour ces éléments). e.target.value est alors typé string.

Faut-il utiliser React.FC ?

C'est facultatif. Typer directement le paramètre des props (function Card({ title }: CardProps)) fait le même travail, se lit plus simplement et fonctionne avec les génériques. React.FC n'ajoute plus children implicitement, il apporte donc peu de chose.

Faut-il utiliser type ou interface pour les props ?

Les deux fonctionnent. interface peut être étendue et fusionnée ; type peut exprimer des unions et des types mappés. Choisissez une convention pour la base de code et tenez-vous-y.

Illustration des langages de programmation de Coddy

Apprendre à coder avec Coddy

COMMENCER