TypeScript — Aide-mémoire complet

Référence orientée stack React 18 + Redux Toolkit + MUI v7 + Vite, avec consommation d'API Symfony.
1. Les bases du système de types
Types primitifs
let nom: string = "Amar";
let age: number = 30;          // pas de int/float, tout est number
let actif: boolean = true;
let rien: null = null;
let indefini: undefined = undefined;
let gros: bigint = 100n;
let sym: symbol = Symbol("id");

any vs unknown vs never

Type Sens Usage
| any  | désactive le typage  | à éviter — annule tout l'intérêt de TS
| unknown  | valeur inconnue, doit être vérifiée avant usage  | retour d'API non validé, catch (e: unknown)
| never  | ne retourne jamais / cas impossible  | fonctions qui throw, exhaustivité de switch
function parse(json: string): unknown {
  return JSON.parse(json);
}

const data = parse('{"a":1}');
// data.a          // ❌ Erreur : 'data' est unknown
if (typeof data === "object" && data !== null && "a" in data) {
  console.log(data.a); // ✅ narrowing effectué
}

function fail(msg: string): never {
  throw new Error(msg);
}

Inférence
TS déduit le type quand c'est possible — n'annote pas inutilement.
let x = 5;              // number (inféré)
const y = 5;            // 5 (literal type, car const)
const arr = [1, 2, 3];  // number[]
const obj = { a: 1 };   // { a: number }

Règle pratique : annote les signatures de fonction et les frontières (API, props), laisse inférer le reste.
2. Objets et interfaces
type vs interface
interface User {
  id: number;
  email: string;
  nom?: string;          // optionnel
  readonly createdAt: string; // lecture seule
}

type UserAlias = {
  id: number;
  email: string;
};


 interface type
| Extension  | extends  | intersection &
| Declaration merging  | ✅ (réouvrable)  | ❌
| Unions / tuples / mapped types  | ❌  | ✅
| Perf compilateur  | légèrement meilleure  | —
Convention courante : interface pour les formes d'objets publiques (props, entités), type pour tout le reste (unions, utilitaires, alias).
Index signatures
interface Dico {
  [cle: string]: string;
}

// Version plus stricte
type Dico2 = Record<string, string>;

Extension
interface Base { id: number; }
interface Article extends Base { titre: string; }

// Équivalent en type
type ArticleT = Base & { titre: string };

3. Unions, intersections, narrowing
type Statut = "brouillon" | "publie" | "archive"; // union de literals

type Id = string | number;
type Complet = { a: string } & { b: number };     // intersection

Discriminated unions — le pattern le plus utile
type Etat =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: User[] }
  | { status: "error"; message: string };

function render(e: Etat) {
  switch (e.status) {
    case "idle":    return null;
    case "loading": return "Chargement…";
    case "success": return e.data.length; // ✅ data existe ici
    case "error":   return e.message;     // ✅ message existe ici
    default: {
      const _exhaustive: never = e; // ✅ erreur si un cas est oublié
      return _exhaustive;
    }
  }
}

C'est exactement le pattern à appliquer aux états de fetch dans Redux.
Techniques de narrowing
typeof x === "string"            // primitifs
x instanceof Date                // classes
"prop" in obj                    // présence de propriété
Array.isArray(x)                 // tableaux
x !== null && x !== undefined    // nullish

Type guards personnalisés
function isUser(x: unknown): x is User {
  return typeof x === "object" && x !== null && "id" in x && "email" in x;
}

if (isUser(data)) {
  data.email; // ✅ typé User
}

Assertion functions
function assertIsUser(x: unknown): asserts x is User {
  if (!isUser(x)) throw new Error("Pas un User");
}

assertIsUser(data);
data.email; // ✅ typé après l'appel

4. Fonctions
function add(a: number, b: number): number { return a + b; }

const mul = (a: number, b: number): number => a * b;

// Paramètres optionnels et valeurs par défaut
function greet(nom: string, prefix = "M."): string {
  return `${prefix} ${nom}`;
}

// Rest
function sum(...n: number[]): number {
  return n.reduce((a, b) => a + b, 0);
}

// Type de fonction
type Handler = (event: string, payload?: unknown) => void;

Surcharges
function get(id: number): User;
function get(email: string): User;
function get(arg: number | string): User {
  // implémentation
}

5. Génériques
function first<T>(arr: T[]): T | undefined {
  return arr[0];
}

first([1, 2, 3]);      // number | undefined
first(["a", "b"]);     // string | undefined

Contraintes
function getProp<T, K extends keyof T>(obj: T, cle: K): T[K] {
  return obj[cle];
}

getProp({ a: 1, b: "x" }, "a"); // number
getProp({ a: 1 }, "z");         // ❌ Erreur

Valeurs par défaut
interface ApiResponse<T = unknown> {
  data: T;
  status: number;
}

Interfaces génériques réutilisables
interface Paginated<T> {
  items: T[];
  total: number;
  page: number;
  perPage: number;
}

type UsersPage = Paginated<User>;

6. Types utilitaires (built-in)
interface User {
  id: number;
  email: string;
  nom: string;
  role: "admin" | "user";
}


Utilitaire Résultat
| Partial<User>  | toutes les props optionnelles
| Required<User>  | toutes obligatoires
| Readonly<User>  | toutes en lecture seule
| Pick<User, "id" \| "email">  | { id, email }
| Omit<User, "id">  | { email, nom, role }
| Record<string, User>  | dictionnaire
| Exclude<User["role"], "admin">  | "user"
| Extract<Id, string>  | garde les membres assignables
| NonNullable<T>  | retire null \| undefined
| ReturnType<typeof fn>  | type de retour
| Parameters<typeof fn>  | tuple des params
| Awaited<Promise<User>>  | User
| InstanceType<typeof Class>  | instance
Cas typiques :
type CreateUserDto = Omit<User, "id">;
type UpdateUserDto = Partial<Omit<User, "id">>;
type UserPreview = Pick<User, "id" | "nom">;

7. Types avancés
keyof, typeof, indexed access
type UserKeys = keyof User;        // "id" | "email" | "nom" | "role"
type Role = User["role"];          // "admin" | "user"

const config = { url: "/api", timeout: 5000 };
type Config = typeof config;       // { url: string; timeout: number }

Mapped types
type Nullable<T> = { [K in keyof T]: T[K] | null };
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
// { getId: () => number; getEmail: () => string; ... }

Conditional types
type IsArray<T> = T extends unknown[] ? true : false;

type Unwrap<T> = T extends Promise<infer U> ? U : T;
type A = Unwrap<Promise<string>>; // string

Template literal types
type Endpoint = `/api/${string}`;
type Method = "GET" | "POST";
type Route = `${Method} ${Endpoint}`; // "GET /api/users" etc.

satisfies (TS 4.9+)
Valide sans élargir le type inféré — très utile pour les configs.
const routes = {
  home: "/",
  users: "/users",
} satisfies Record<string, `/${string}`>;

routes.home; // type "/" (literal préservé), pas string

as const
const ROLES = ["admin", "user"] as const;
type Role = typeof ROLES[number]; // "admin" | "user"

8. React + TypeScript
Composants et props
interface ButtonProps {
  label: string;
  variant?: "primary" | "secondary";
  onClick: () => void;
  children?: React.ReactNode;
}

// Préférer une fonction typée directement (pas React.FC)
function Button({ label, variant = "primary", onClick }: ButtonProps) {
  return <button onClick={onClick}>{label}</button>;
}

React.FC est déconseillé aujourd'hui : il ajoutait un children implicite (supprimé en React 18) et gêne les génériques.
Hooks
const [count, setCount] = useState(0);                    // inféré : number
const [user, setUser] = useState<User | null>(null);      // annotation nécessaire
const ref = useRef<HTMLInputElement>(null);               // ref DOM
const timer = useRef<number | undefined>(undefined);      // ref mutable

const memo = useMemo(() => compute(a, b), [a, b]);
const cb = useCallback((id: number) => void load(id), []);

Événements
function onChange(e: React.ChangeEvent<HTMLInputElement>) {
  setValue(e.target.value);
}
function onSubmit(e: React.FormEvent<HTMLFormElement>) {
  e.preventDefault();
}
function onClick(e: React.MouseEvent<HTMLButtonElement>) {}

Composant générique
interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
  keyOf: (item: T) => string | number;
}

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

Étendre des props HTML
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
  erreur?: string;
}

9. Redux Toolkit + TypeScript
Store et hooks typés
// store.ts
import { configureStore } from "@reduxjs/toolkit";

export const store = configureStore({
  reducer: { users: usersReducer, tickets: ticketsReducer },
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

// hooks.ts import { useDispatch, useSelector } from "react-redux"; import type { RootState, AppDispatch } from "./store"; export const useAppDispatch = useDispatch.withTypes<AppDispatch>(); export const useAppSelector = useSelector.withTypes<RootState>();
À importer partout à la place de useDispatch/useSelector bruts.
Slice typé
interface UsersState {
  items: User[];
  status: "idle" | "loading" | "succeeded" | "failed";
  error: string | null;
}

const initialState: UsersState = { items: [], status: "idle", error: null };

const usersSlice = createSlice({
  name: "users",
  initialState,
  reducers: {
    userAdded(state, action: PayloadAction<User>) {
      state.items.push(action.payload);
    },
    userRemoved(state, action: PayloadAction<number>) {
      state.items = state.items.filter(u => u.id !== action.payload);
    },
  },
  extraReducers: (builder) => {
    builder
      .addCase(fetchUsers.pending, (state) => { state.status = "loading"; })
      .addCase(fetchUsers.fulfilled, (state, action) => {
        state.status = "succeeded";
        state.items = action.payload;
      })
      .addCase(fetchUsers.rejected, (state, action) => {
        state.status = "failed";
        state.error = action.error.message ?? "Erreur inconnue";
      });
  },
});

export const { userAdded, userRemoved } = usersSlice.actions;
export default usersSlice.reducer;

createAsyncThunk typé
export const fetchUsers = createAsyncThunk<
  User[],                                  // type de retour
  void,                                    // argument
  { state: RootState; rejectValue: string } // thunkApi
>("users/fetch", async (_, { rejectWithValue }) => {
  try {
    const res = await fetch("/api/users");
    if (!res.ok) return rejectWithValue(`HTTP ${res.status}`);
    return (await res.json()) as User[];
  } catch (e) {
    return rejectWithValue(e instanceof Error ? e.message : "Erreur réseau");
  }
});

Pour éviter de répéter les génériques, définis une fois :
export const createAppAsyncThunk = createAsyncThunk.withTypes<{
  state: RootState;
  dispatch: AppDispatch;
  rejectValue: string;
}>();

Sélecteurs
export const selectUsers = (state: RootState) => state.users.items;

export const selectAdmins = createSelector(
  [selectUsers],
  (users) => users.filter(u => u.role === "admin")
);

10. MUI v7 + TypeScript
Étendre le thème
// theme.d.ts
import "@mui/material/styles";

declare module "@mui/material/styles" {
  interface Palette {
    marque: Palette["primary"];
  }
  interface PaletteOptions {
    marque?: PaletteOptions["primary"];
  }
  interface Theme {
    custom: { headerHeight: number };
  }
  interface ThemeOptions {
    custom?: { headerHeight?: number };
  }
}

Nouvelle variante de composant
declare module "@mui/material/Button" {
  interface ButtonPropsVariantOverrides {
    fantome: true;
  }
}
// <Button variant="fantome" /> ✅

Composant sx typé
import type { SxProps, Theme } from "@mui/material";

const styles: SxProps<Theme> = {
  p: 2,
  color: (t) => t.palette.primary.main,
};

styled avec props custom
const Box = styled("div", {
  shouldForwardProp: (prop) => prop !== "actif",
})<{ actif: boolean }>(({ theme, actif }) => ({
  opacity: actif ? 1 : 0.5,
  padding: theme.spacing(2),
}));

11. Typer les API Symfony
Option A — génération depuis OpenAPI (recommandé)
Si tu exposes NelmioApiDoc / API Platform :
npm i -D openapi-typescript
npx openapi-typescript https://api.exemple.fr/api/doc.json -o src/types/api.d.ts

import type { components } from "./types/api"; type User = components["schemas"]["User"];
Avantage : la source de vérité reste le backend. Un changement d'entité casse le build front.
Option B — validation runtime avec Zod
Nécessaire dès que tu ne fais pas confiance à la forme reçue.
import { z } from "zod";

const UserSchema = z.object({
  id: z.number(),
  email: z.string().email(),
  nom: z.string(),
  role: z.enum(["admin", "user"]),
});

export type User = z.infer<typeof UserSchema>; // type dérivé du schéma

export async function fetchUser(id: number): Promise<User> {
  const res = await fetch(`/api/users/${id}`);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return UserSchema.parse(await res.json()); // throw si la forme est mauvaise
}

Le piège à connaître
const res = await fetch("/api/users");
const data = await res.json(); // type: any ⚠️

res.json() retourne any. Un simple as User[] ne vérifie rien — c'est un mensonge au compilateur. Soit tu génères depuis OpenAPI (contrat fiable), soit tu valides avec Zod. L'assertion as seule ne fait que déplacer le bug plus loin.
12. Configuration
tsconfig.json — migration progressive
{
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["ES2022", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "moduleResolution": "bundler",
    "jsx": "react-jsx",

    "allowJs": true,          // cohabitation .js/.ts pendant la migration
    "checkJs": false,
    "strict": false,          // ⚠️ passer à true une fois migré
    "noEmit": true,           // Vite gère la transpilation
    "isolatedModules": true,  // requis par esbuild/Vite
    "skipLibCheck": true,
    "esModuleInterop": true,
    "resolveJsonModule": true,

    "baseUrl": ".",
    "paths": { "@/*": ["src/*"] }
  },
  "include": ["src"]
}

tsconfig strict — cible finale
{
  "compilerOptions": {
    "strict": true,                        // active les 8 flags ci-dessous
    "noUncheckedIndexedAccess": true,      // arr[0] devient T | undefined
    "noImplicitOverride": true,
    "exactOptionalPropertyTypes": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true
  }
}

Ce que strict: true active : noImplicitAny, strictNullChecks, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, noImplicitThis, alwaysStrict, useUnknownInCatchVariables.
Le plus impactant est strictNullChecks — c'est lui qui trouve les vrais bugs.
Vite
Vite transpile le TS via esbuild mais ne vérifie pas les types. Il faut donc :
{
  "scripts": {
    "dev": "vite",
    "build": "tsc --noEmit && vite build",
    "typecheck": "tsc --noEmit --watch"
  }
}

Ou le plugin vite-plugin-checker pour avoir les erreurs dans l'overlay dev.
13. Plan de migration pour hdr-admin
  1. Installer : npm i -D typescript @types/react @types/react-dom
  2. Créer tsconfig.json avec allowJs: true, strict: false
  3. Renommer vite.config.js → vite.config.ts
  4. Typer d'abord les frontières — le meilleur ratio effort/bénéfice :
  5. src/types/api.ts (entités Ticket, TimeEntry, User…)
  6. store.ts → RootState, AppDispatch
  7. hooks.ts → useAppSelector, useAppDispatch
  8. Migrer les slices un par un en .ts (peu de JSX, gros gain de sûreté)
  9. Nouveaux composants en .tsx, anciens laissés en .jsx
  10. Migrer les composants du plus feuille vers le plus racine
  11. Activer strict: true une fois allowJs retirable, puis noUncheckedIndexedAccess
  12. Ajouter tsc --noEmit au build et en CI
Ordre important : jamais de big-bang. Chaque étape doit laisser l'app fonctionnelle.
14. Erreurs fréquentes

Message Cause Correction
| Object is possibly 'null'  | strictNullChecks  | optional chaining ?., garde, ou ! si certain
| Property 'x' does not exist on type 'never'  | narrowing a tout éliminé  | vérifier le discriminant de l'union
| Type 'string' is not assignable to type '"a" \| "b"'  | literal élargi en string  | as const ou annotation explicite
| Cannot find module 'x' or its type declarations  | pas de types pour la lib  | npm i -D @types/x ou declare module "x";
| Element implicitly has an 'any' type because index expression...  | index signature manquante  | Record<string, T> ou keyof
| JSX element type does not have any construct signatures  | composant mal typé  | vérifier le retour du composant
15. Anti-patterns
// ❌ any partout — annule TypeScript
const data: any = await res.json();

// ❌ assertion mensongère — aucune vérification au runtime
const user = JSON.parse(s) as User;

// ❌ ! systématique — masque les null réels
const el = document.getElementById("x")!;

// ❌ @ts-ignore — supprime l'erreur sans la comprendre
// @ts-ignore
foo.bar();

// ✅ Alternatives
const data: unknown = await res.json();
const user = UserSchema.parse(JSON.parse(s));
const el = document.getElementById("x");
if (!el) throw new Error("Élément introuvable");
// @ts-expect-error — signale une erreur attendue ; le compilateur alerte si elle disparaît

@ts-expect-error est toujours préférable à @ts-ignore : il devient lui-même une erreur si le problème est résolu, ce qui évite les suppressions fantômes.
16. Ressources
  • Handbook officiel — typescriptlang.org/docs/handbook
  • React TypeScript Cheatsheet — react-typescript-cheatsheet.netlify.app
  • Redux Toolkit + TS — redux-toolkit.js.org/usage/usage-with-typescript
  • Type Challenges — github.com/type-challenges/type-challenges (exercices de types avancés)
  • Total TypeScript — totaltypescript.com (tutoriels gratuits de Matt Pocock)
  • MUI TypeScript — mui.com/material-ui/guides/typescript