React + TypeScript : typer props et hooks

Front-end 25/03/2026 13:00:00 angularforall.com
React Typescript Props Hooks Generics
React + TypeScript : typer props et hooks

Guide complet pour typer vos composants React avec TypeScript : props, hooks, events, generics et patterns avancés pour débutants et juniors.

Pourquoi TypeScript avec React ?

TypeScript est devenu le standard de facto pour les projets React professionnels. En 2024, plus de 80% des nouveaux projets React sont initialisés avec TypeScript. Pourquoi cet engouement ?

La réponse tient en trois bénéfices concrets :

  • Autocomplétion intelligente : votre IDE sait exactement quelles props sont disponibles, quels types sont attendus, quelles méthodes existent
  • Erreurs détectées à la compilation : passer un number là où une string est attendue est une erreur capturée avant même d'ouvrir le navigateur
  • Documentation vivante : les types servent de contrat entre les composants — plus besoin de lire la doc pour savoir ce qu'accepte un composant
// Sans TypeScript : aucune aide de l'IDE, erreurs silencieuses
function CarteUtilisateur({ user, onDelete }) {
    // On ne sait pas : user a-t-il un champ "name" ou "nom" ?
    // onDelete reçoit-il l'id ou l'objet entier ?
    return <div>{user.name}</div>;
}

// Avec TypeScript : contrat explicite, autocomplétion complète
interface Utilisateur {
    id:     number;
    nom:    string;
    email:  string;
    actif:  boolean;
}

interface CarteUtilisateurProps {
    user:     Utilisateur;
    onDelete: (id: number) => void;
}

function CarteUtilisateur({ user, onDelete }: CarteUtilisateurProps) {
    // IDE sait que user.nom existe, que onDelete attend un number
    return (
        <div>
            <p>{user.nom}</p>
            <button onClick={() => onDelete(user.id)}>Supprimer</button>
        </div>
    );
}
Créer un projet React + TypeScript :
npm create vite@latest mon-app -- --template react-ts
Vite configure automatiquement TypeScript, les types React et le tsconfig optimisé.
Situation Sans TypeScript Avec TypeScript
Prop manquante Erreur silencieuse en runtime Erreur rouge dans l'IDE
Mauvais type de prop Bug difficile à tracer Détecté à la compilation
Refactoring de prop Chercher manuellement tous les usages TS signale tous les endroits à corriger
Onboarding nouveau dev Lire la doc ou le code source Les types documentent le contrat

Typer les props d'un composant

Il existe deux syntaxes pour définir le type des props : interface et type. Les deux fonctionnent, mais la convention React préfère interface pour les props (extensible) et type pour les unions et types composés.

Avec interface (recommandé pour les props)

// Convention : suffixe "Props" pour les interfaces de props
interface BoutonProps {
    label:    string;           // Requis, chaîne de caractères
    variante: 'primary' | 'secondary' | 'danger'; // Union littérale
    taille:   'sm' | 'md' | 'lg';
    disabled?: boolean;         // Optionnel (le ? signifie que c'est facultatif)
    onClick:  () => void;       // Fonction sans argument, sans retour
}

function Bouton({ label, variante, taille, disabled = false, onClick }: BoutonProps) {
    // disabled a une valeur par défaut : false si non fourni
    const classes = `btn btn-${variante} btn-${taille}`;
    return (
        <button className={classes} disabled={disabled} onClick={onClick}>
            {label}
        </button>
    );
}

// Utilisation — TypeScript valide les props à la compilation
<Bouton
    label="Enregistrer"
    variante="primary"
    taille="md"
    onClick={() => console.log('clic')}
/>

// ❌ Erreur TypeScript : "xl" n'est pas dans 'sm' | 'md' | 'lg'
<Bouton label="Test" variante="primary" taille="xl" onClick={() => {}} />

Avec type (pour les unions et cas complexes)

// type est plus flexible pour les unions discriminées
type StatutCommande =
    | { statut: 'en_attente' }
    | { statut: 'expedie';  transporteur: string; numeroSuivi: string }
    | { statut: 'livre';    datelivraison: Date }
    | { statut: 'annule';   raisonAnnulation: string };

// TypeScript sait exactement quels champs sont disponibles selon le statut
function BadgeStatut({ statut }: { statut: StatutCommande }) {
    switch (statut.statut) {
        case 'expedie':
            // Ici TypeScript sait que transporteur et numeroSuivi existent
            return <span>Expédié par {statut.transporteur} — {statut.numeroSuivi}</span>;
        case 'livre':
            // Ici TypeScript sait que datelivraison existe
            return <span>Livré le {statut.datelivraison.toLocaleDateString()}</span>;
        case 'annule':
            return <span>Annulé : {statut.raisonAnnulation}</span>;
        default:
            return <span>En attente</span>;
    }
}

Étendre une interface existante

// Interface de base pour tous les éléments de formulaire
interface ChampBaseProps {
    id:          string;
    label:       string;
    erreur?:     string;
    obligatoire?: boolean;
}

// Étendre pour un input texte — hérite de toutes les props de base
// HTMLInputElement donne accès à toutes les props natives de <input>
interface ChampTexteProps extends ChampBaseProps,
    Omit<React.InputHTMLAttributes<HTMLInputElement>, 'id'> {
    type?: 'text' | 'email' | 'password' | 'tel';
}

function ChampTexte({ id, label, erreur, obligatoire, type = 'text', ...reste }: ChampTexteProps) {
    return (
        <div className="mb-3">
            <label htmlFor={id}>
                {label}
                {obligatoire && <span aria-hidden="true"> *</span>}
            </label>
            <input
                id={id}
                type={type}
                className={`form-control ${erreur ? 'is-invalid' : ''}`}
                aria-invalid={!!erreur}
                aria-describedby={erreur ? `${id}-erreur` : undefined}
                {...reste} // Toutes les props HTML natives (value, onChange, placeholder…)
            />
            {erreur && (
                <div id={`${id}-erreur`} className="invalid-feedback">
                    {erreur}
                </div>
            )}
        </div>
    );
}

Props avancées : children, callbacks, optionnelles

Typer children correctement

import { ReactNode, PropsWithChildren } from 'react';

// Option 1 : ReactNode (le plus permissif — accepte tout ce que React peut rendre)
interface CarteProps {
    titre:    string;
    children: ReactNode; // string, JSX, tableau, null, undefined...
}

// Option 2 : PropsWithChildren (raccourci pratique)
// Équivalent à { children?: ReactNode } + vos props
function Carte({ titre, children }: PropsWithChildren<{ titre: string }>) {
    return (
        <div className="card">
            <div className="card-header"><h3>{titre}</h3></div>
            <div className="card-body">{children}</div>
        </div>
    );
}

// Option 3 : ReactElement (uniquement des éléments JSX, pas de string)
interface LayoutProps {
    header:  React.ReactElement; // Doit être un composant JSX
    sidebar: React.ReactElement;
    main:    React.ReactElement;
}

Typer les fonctions de callback

// Différents types de callbacks selon le besoin

interface TableauProps<T> {
    items:    T[];

    // Callback sans retour
    onSelect: (item: T) => void;

    // Callback avec retour booléen
    onDelete: (id: number) => boolean;

    // Callback asynchrone
    onSave:   (item: T) => Promise<void>;

    // Callback optionnel
    onSort?:  (colonne: keyof T, ordre: 'asc' | 'desc') => void;

    // Render prop : fonction qui retourne du JSX
    renderItem: (item: T, index: number) => React.ReactNode;
}

// Exemple d'utilisation avec un render prop
function Liste<T extends { id: number }>({ items, renderItem }: TableauProps<T>) {
    return (
        <ul>
            {items.map((item, index) => (
                <li key={item.id}>{renderItem(item, index)}</li>
            ))}
        </ul>
    );
}

Props discriminées selon un type

// Pattern : props différentes selon une prop "variante"
// Évite de rendre toutes les props optionnelles (moins sûr)

type AlerteProps =
    | {
        type:    'succes';
        message: string;
        // Pas de prop "details" pour les succès
      }
    | {
        type:    'erreur';
        message: string;
        details: string;   // Obligatoire uniquement pour les erreurs
        onReessayer?: () => void;
      }
    | {
        type:    'info';
        message: string;
        lien?:   string; // Optionnel uniquement pour les infos
      };

function Alerte(props: AlerteProps) {
    const classes = {
        succes: 'alert alert-success',
        erreur: 'alert alert-danger',
        info:   'alert alert-info',
    }[props.type];

    return (
        <div className={classes} role="alert">
            <p>{props.message}</p>
            {/* TypeScript sait que details n'existe que pour type='erreur' */}
            {props.type === 'erreur' && <small>{props.details}</small>}
            {props.type === 'erreur' && props.onReessayer && (
                <button onClick={props.onReessayer}>Réessayer</button>
            )}
        </div>
    );
}

Typer useState, useRef et useReducer

useState avec inférence automatique

import { useState } from 'react';

// TypeScript infère le type depuis la valeur initiale
const [compteur, setCompteur] = useState(0);        // number
const [nom, setNom]           = useState('');        // string
const [actif, setActif]       = useState(false);     // boolean

// Pour les types complexes, fournir le type générique explicitement
interface Utilisateur {
    id:    number;
    nom:   string;
    email: string;
}

// Sans annotation : TypeScript infère Utilisateur | null
const [user, setUser] = useState<Utilisateur | null>(null);

// Avec assertion : si vous êtes sûr que user ne sera jamais null après init
// (à utiliser avec précaution)
const [config, setConfig] = useState<Utilisateur>({} as Utilisateur);

// Tableau typé
const [articles, setArticles] = useState<Utilisateur[]>([]);
// Équivalent : useState([] as Utilisateur[])

useRef : deux usages, deux types

import { useRef } from 'react';

// Usage 1 : référencer un élément DOM
// HTMLInputElement, HTMLDivElement, HTMLButtonElement, HTMLFormElement...
function ChampRecherche() {
    // null comme valeur initiale = élément pas encore monté
    const inputRef = useRef<HTMLInputElement>(null);

    const focuser = () => {
        // inputRef.current peut être null avant le montage
        // L'opérateur ?. gère ce cas proprement
        inputRef.current?.focus();
        inputRef.current?.select(); // Sélectionner tout le texte
    };

    return (
        <div>
            {/* ref= attend RefObject<HTMLInputElement> — types compatibles */}
            <input ref={inputRef} type="text" placeholder="Rechercher..." />
            <button onClick={focuser}>Focuser</button>
        </div>
    );
}

// Usage 2 : valeur mutable qui ne déclenche pas de re-rendu
function CompteurRendus() {
    const [etat, setEtat] = useState(0);
    // MutableRefObject<number> — pas de null car valeur initiale fournie
    const rendus = useRef<number>(0);

    rendus.current += 1; // Incrémenter sans déclencher de re-rendu

    return (
        <div>
            <p>État : {etat} — Rendus totaux : {rendus.current}</p>
            <button onClick={() => setEtat(e => e + 1)}>Incrémenter</button>
        </div>
    );
}

useReducer avec types stricts

import { useReducer } from 'react';

// Typer l'état
interface EtatPanier {
    items:      ProduitPanier[];
    promo:      string | null;
    chargement: boolean;
}

// Union discriminée pour les actions — pattern recommandé
type ActionPanier =
    | { type: 'AJOUTER_ITEM';   payload: ProduitPanier }
    | { type: 'RETIRER_ITEM';   payload: number }          // id du produit
    | { type: 'APPLIQUER_PROMO'; payload: string }
    | { type: 'RETIRER_PROMO' }                            // pas de payload
    | { type: 'VIDER' };

interface ProduitPanier {
    id:       number;
    nom:      string;
    prix:     number;
    quantite: number;
}

// Le reducer est fortement typé — TypeScript vérifie chaque case
function panierReducer(etat: EtatPanier, action: ActionPanier): EtatPanier {
    switch (action.type) {
        case 'AJOUTER_ITEM':
            // TypeScript sait que action.payload est ProduitPanier
            return { ...etat, items: [...etat.items, action.payload] };

        case 'RETIRER_ITEM':
            // TypeScript sait que action.payload est number (l'id)
            return { ...etat, items: etat.items.filter(i => i.id !== action.payload) };

        case 'APPLIQUER_PROMO':
            return { ...etat, promo: action.payload };

        case 'RETIRER_PROMO':
            // Pas de payload — TypeScript vérifie qu'on n'en accède pas
            return { ...etat, promo: null };

        case 'VIDER':
            return { ...etat, items: [], promo: null };

        default:
            // Exhaustivité vérifiée par TypeScript
            const _exhaustif: never = action;
            return etat;
    }
}

function Panier() {
    const [etat, dispatch] = useReducer(panierReducer, {
        items:      [],
        promo:      null,
        chargement: false,
    });

    return (
        <div>
            <p>{etat.items.length} article(s)</p>
            <button onClick={() => dispatch({ type: 'VIDER' })}>
                Vider
            </button>
        </div>
    );
}
Astuce — vérification d'exhaustivité : Le cas default: const _exhaustif: never = action force TypeScript à vérifier que tous les types d'action sont gérés. Si vous ajoutez un nouveau type d'action sans l'ajouter au switch, TypeScript signale une erreur.

Typer les événements DOM

Les événements React sont génériques. Chaque type d'événement a son propre type TypeScript. Voici le guide complet des événements les plus courants.

import { ChangeEvent, FormEvent, MouseEvent, KeyboardEvent, FocusEvent } from 'react';

function FormulaireComplet() {
    // --- Input / Select / Textarea ---
    const handleInput = (e: ChangeEvent<HTMLInputElement>) => {
        const valeur: string  = e.target.value;
        const checked: boolean = e.target.checked; // Pour les checkboxes
        const fichiers         = e.target.files;    // Pour type="file"
    };

    const handleSelect = (e: ChangeEvent<HTMLSelectElement>) => {
        const valeur: string = e.target.value;
        // Pour les selects multiples
        const valeurs: string[] = Array.from(e.target.selectedOptions, o => o.value);
    };

    const handleTextarea = (e: ChangeEvent<HTMLTextAreaElement>) => {
        const texte: string = e.target.value;
    };

    // --- Formulaire ---
    const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
        e.preventDefault(); // Empêcher le rechargement de la page
        const formData = new FormData(e.currentTarget);
        const email    = formData.get('email') as string;
    };

    // --- Clic ---
    const handleClick = (e: MouseEvent<HTMLButtonElement>) => {
        e.stopPropagation(); // Empêcher la propagation
        const { clientX, clientY } = e; // Position du curseur
    };

    // --- Clavier ---
    const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
        if (e.key === 'Enter') { /* soumettre */ }
        if (e.key === 'Escape') { /* fermer */ }
        if (e.ctrlKey && e.key === 's') { /* sauvegarder */ }
    };

    // --- Focus ---
    const handleFocus = (e: FocusEvent<HTMLInputElement>) => {
        e.target.select(); // Sélectionner tout le texte au focus
    };

    return (
        <form onSubmit={handleSubmit}>
            <input onChange={handleInput}   onKeyDown={handleKeyDown} onFocus={handleFocus} />
            <select onChange={handleSelect}></select>
            <textarea onChange={handleTextarea}></textarea>
            <button type="submit" onClick={handleClick}>Envoyer</button>
        </form>
    );
}

Tableau de référence des types d'événements

Événement React Type TypeScript Élément typique
onChange ChangeEvent<HTMLInputElement> input, select, textarea
onSubmit FormEvent<HTMLFormElement> form
onClick MouseEvent<HTMLButtonElement> button, div, a
onKeyDown KeyboardEvent<HTMLInputElement> input, div (tabindex)
onFocus / onBlur FocusEvent<HTMLInputElement> input, textarea
onDragStart DragEvent<HTMLDivElement> div draggable
onScroll UIEvent<HTMLDivElement> div scrollable

Composants génériques avec TypeScript

Les composants génériques permettent de créer des composants réutilisables qui fonctionnent avec n'importe quel type tout en conservant la sécurité de type. C'est l'un des patterns les plus puissants de React + TypeScript.

Liste générique typée

// T est le type des éléments — déterminé lors de l'utilisation
interface ListeProps<T> {
    items:       T[];
    getKey:      (item: T) => string | number; // Fonction pour extraire la clé
    renderItem:  (item: T) => React.ReactNode; // Render prop générique
    itemVide?:   React.ReactNode;              // Contenu quand la liste est vide
}

// La syntaxe <T,> (avec virgule) est nécessaire dans les fichiers .tsx
// pour que TypeScript ne confonde pas <T> avec du JSX
function Liste<T,>({ items, getKey, renderItem, itemVide }: ListeProps<T>) {
    if (items.length === 0) {
        return <div className="text-muted">{itemVide ?? 'Aucun élément'}</div>;
    }
    return (
        <ul className="list-group">
            {items.map(item => (
                <li key={getKey(item)} className="list-group-item">
                    {renderItem(item)}
                </li>
            ))}
        </ul>
    );
}

// Utilisation avec des Utilisateurs — T = Utilisateur
<Liste
    items={utilisateurs}
    getKey={u => u.id}
    renderItem={u => <span>{u.nom} — {u.email}</span>}
    itemVide={<p>Aucun utilisateur trouvé</p>}
/>

// Utilisation avec des Produits — T = Produit
<Liste
    items={produits}
    getKey={p => p.reference}
    renderItem={p => <span>{p.nom} — {p.prix}€</span>}
/>

Sélecteur générique (Select dropdown)

// Composant Select réutilisable pour n'importe quel type d'option
interface SelectProps<T,> {
    options:      T[];
    valeur:       T | null;
    onChangement: (nouvelleValeur: T) => void;
    getLabel:     (option: T) => string;  // Texte affiché dans le select
    getValeur:    (option: T) => string;  // Valeur soumise dans le formulaire
    placeholder?: string;
    disabled?:    boolean;
}

function Select<T,>({
    options,
    valeur,
    onChangement,
    getLabel,
    getValeur,
    placeholder = 'Sélectionner...',
    disabled    = false,
}: SelectProps<T>) {
    const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
        const valeurSelectionnee = e.target.value;
        // Retrouver l'objet complet depuis la valeur de l'option
        const option = options.find(o => getValeur(o) === valeurSelectionnee);
        if (option) onChangement(option);
    };

    return (
        <select
            className="form-select"
            value={valeur ? getValeur(valeur) : ''}
            onChange={handleChange}
            disabled={disabled}
        >
            <option value="">{placeholder}</option>
            {options.map(option => (
                <option key={getValeur(option)} value={getValeur(option)}>
                    {getLabel(option)}
                </option>
            ))}
        </select>
    );
}

// Utilisation avec des pays
interface Pays { code: string; nom: string; }
const pays: Pays[] = [{ code: 'FR', nom: 'France' }, { code: 'ES', nom: 'Espagne' }];

<Select<Pays>
    options={pays}
    valeur={paysSelectionne}
    onChangement={setPaysSelectionne}
    getLabel={p => p.nom}
    getValeur={p => p.code}
/>

Typer le Context API

Le Context API requiert une attention particulière avec TypeScript. Voici le pattern le plus robuste pour typer un contexte sans compromettre la sécurité des types.

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

// 1. Définir le type de la valeur du contexte
interface ThemeContextType {
    theme:        'light' | 'dark';
    couleurPrincipale: string;
    toggleTheme:  () => void;
    setCouleur:   (couleur: string) => void;
}

// 2. Créer le contexte avec null comme valeur initiale
// null signifie "pas encore dans un Provider"
const ThemeContext = createContext<ThemeContextType | null>(null);

// 3. Hook avec vérification de type
export function useTheme(): ThemeContextType {
    const contexte = useContext(ThemeContext);

    // Assertion de type : si null, l'usage est incorrect
    if (contexte === null) {
        throw new Error(
            'useTheme() doit être appelé à l\'intérieur d\'un <ThemeProvider>.'
        );
    }

    // TypeScript sait maintenant que contexte est ThemeContextType (pas null)
    return contexte;
}

// 4. Provider typé
interface ThemeProviderProps {
    children:         React.ReactNode;
    themeInitial?:    'light' | 'dark'; // Prop optionnelle avec valeur par défaut
    couleurInitiale?: string;
}

export function ThemeProvider({
    children,
    themeInitial  = 'light',
    couleurInitiale = '#0d6efd',
}: ThemeProviderProps) {
    const [theme, setTheme]   = useState<'light' | 'dark'>(themeInitial);
    const [couleur, setCouleur] = useState<string>(couleurInitiale);

    const valeur = useMemo<ThemeContextType>(() => ({
        theme,
        couleurPrincipale: couleur,
        toggleTheme: () => setTheme(t => t === 'light' ? 'dark' : 'light'),
        setCouleur,
    }), [theme, couleur]);

    return (
        <ThemeContext.Provider value={valeur}>
            {children}
        </ThemeContext.Provider>
    );
}

Erreurs fréquentes et solutions

Erreur 1 — Utiliser any au lieu du bon type

// ❌ any désactive TypeScript — aucune sécurité
function Composant({ data }: { data: any }) {
    return <p>{data.proprieteInexistante}</p>; // Pas d'erreur, bug en runtime
}

// ✅ Typer précisément ou utiliser unknown avec une assertion
interface DonneesAPI {
    id:    number;
    titre: string;
}

function Composant({ data }: { data: DonneesAPI }) {
    return <p>{data.titre}</p>; // TypeScript vérifie que titre existe
}

// Pour des données vraiment inconnues : unknown + type guard
function traiterDonnees(data: unknown): string {
    if (typeof data === 'string') return data;
    if (typeof data === 'object' && data !== null && 'titre' in data) {
        return (data as DonneesAPI).titre;
    }
    return 'Données inconnues';
}

Erreur 2 — Confondre HTMLElement et les types React

// ❌ HTMLElement est trop générique pour un ref sur un input
const ref = useRef<HTMLElement>(null);
ref.current?.focus(); // OK mais pas d'autocomplétion des props d'input

// ✅ Utiliser le type exact de l'élément ciblé
const inputRef  = useRef<HTMLInputElement>(null);
const divRef    = useRef<HTMLDivElement>(null);
const formRef   = useRef<HTMLFormElement>(null);
const videoRef  = useRef<HTMLVideoElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);

// Maintenant l'autocomplétion fonctionne
inputRef.current?.select();  // Méthode spécifique à HTMLInputElement
formRef.current?.reset();    // Méthode spécifique à HTMLFormElement
videoRef.current?.play();    // Méthode spécifique à HTMLVideoElement

Erreur 3 — Oublier les types pour les props de spread

// ❌ TypeScript ne sait pas ce que contient ...reste
function BoutonCustom({ label, ...reste }) {
    return <button {...reste}>{label}</button>;
}

// ✅ Typer les props natives avec React.ButtonHTMLAttributes
interface BoutonCustomProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
    label:   string;
    variante?: 'primary' | 'secondary';
}

function BoutonCustom({ label, variante = 'primary', className, ...reste }: BoutonCustomProps) {
    const classes = `btn btn-${variante} ${className ?? ''}`.trim();
    // ...reste contient toutes les props natives de <button> correctement typées
    return <button className={classes} {...reste}>{label}</button>;
}

// Maintenant toutes les props HTML natives sont disponibles et typées
<BoutonCustom
    label="Enregistrer"
    variante="primary"
    disabled={enChargement}
    aria-label="Enregistrer le formulaire"
    onClick={handleSubmit}
/>

Erreur 4 — Assertion de type abusive avec "as"

// ❌ Forcer un type avec "as" sans vérification — dangereux
const element = document.getElementById('mon-input') as HTMLInputElement;
element.value = 'test'; // Crash si l'élément n'existe pas ou n'est pas un input

// ✅ Vérifier avant d'asserter
const element = document.getElementById('mon-input');
if (element instanceof HTMLInputElement) {
    // TypeScript sait maintenant que element est HTMLInputElement
    element.value = 'test'; // Sûr
}

// Pour les données d'API : valider avec Zod plutôt qu'asserter
import { z } from 'zod';

const UtilisateurSchema = z.object({
    id:    z.number(),
    nom:   z.string(),
    email: z.string().email(),
});

async function fetchUtilisateur(id: number) {
    const response = await fetch(`/api/users/${id}`);
    const data     = await response.json();
    // Valide ET type en même temps — erreur si les données ne correspondent pas
    return UtilisateurSchema.parse(data);
}

Checklist TypeScript + React

  • Toutes les props ont une interface ou un type explicite (interface XxxProps)
  • Les props optionnelles utilisent ? avec une valeur par défaut dans la déstructuration
  • children est typé React.ReactNode (ou PropsWithChildren)
  • Les événements utilisent le bon type générique (ChangeEvent<HTMLInputElement>…)
  • useRef utilise le type exact de l'élément DOM (HTMLInputElement, non HTMLElement)
  • useState infère le type ou utilise le générique (useState<T | null>(null))
  • Les actions de useReducer sont une union discriminée
  • Le Context est créé avec null et vérifié dans le Hook personnalisé
  • Aucun any sans raison documentée — préférer unknown + type guard
  • Les assertions as sont vérifiées par un instanceof ou une validation Zod
  • Les composants réutilisables passent par des generics <T,> si nécessaire
  • Le fichier tsconfig.json a strict: true activé
Conclusion : TypeScript avec React n'est pas une contrainte — c'est un filet de sécurité qui vous permet de refactorer sans peur, d'onboarder de nouveaux développeurs plus vite, et de détecter des catégories entières de bugs avant qu'ils n'atteignent la production. Commencez par typer les props et les événements, puis progressez vers les génériques et les contextes. Chaque étape apporte immédiatement de la valeur.

Partager