6.4 — TypeScript
Débutant
🎯 Objectif : pouvoir typer correctement une vraie application — fonctions, données API, formulaires, hooks — sans
anyet sans douleur.
À l'issue de cet axe, tu sauras :
- Distinguer interface vs type, union vs intersection
- Utiliser les utility types (Partial, Pick, Omit, Record, ReturnType)
- Manier les génériques pour des fonctions et types réutilisables
- Affiner un type avec narrowing et discriminated unions
- Configurer tsconfig en mode strict
Pourquoi TypeScript ?
Section intitulée « Pourquoi TypeScript ? »3 raisons concrètes :
- Bugs détectés à la compilation : tu sais que
user.emailexiste avant de pousser en prod. - Auto-complétion magique : ton IDE devient ton meilleur ami.
- Documentation vivante : les types sont une doc qui ne ment jamais.
Les langages typés sont devenus la norme côté front (React, Vue, Svelte ont tous une excellente intégration TS) et côté back (Node, Bun, Deno).
Setup minimal
Section intitulée « Setup minimal »npm init -ynpm install -D typescript tsx @types/nodenpx tsc --initUn tsconfig.json pragmatique :
{ "compilerOptions": { "target": "ES2023", "module": "ESNext", "moduleResolution": "Bundler", "strict": true, "noUncheckedIndexedAccess": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true }, "include": ["src/**/*"]}strict: true active 8 vérifications d’un coup. Active aussi noUncheckedIndexedAccess pour que arr[0] soit typé T | undefined (plus sûr).
Annotations de base
Section intitulée « Annotations de base »// Variablesconst name: string = 'Alice';const age: number = 30;const active: boolean = true;const tags: string[] = ['js', 'web'];const tuple: [string, number] = ['Alice', 30];
// Fonctionsfunction add(a: number, b: number): number { return a + b;}
const double = (x: number): number => x * 2;
// Optionnel et défautfunction greet(name: string, formal?: boolean): string { return formal ? `Bonjour ${name}` : `Salut ${name}`;}
function pad(text: string, char: string = ' '): string { return char + text + char;}TypeScript infère la plupart du temps — tu n’as pas besoin d’annoter ce qui est évident :
const name = 'Alice'; // string inféréconst ages = [25, 30, 35]; // number[] inféréconst double = (x: number) => x * 2; // retour inféréBonne pratique : annoter les paramètres de fonction et les types d’API publique ; laisser TypeScript inférer le reste.
interface vs type
Section intitulée « interface vs type »interface User { id: number; name: string; email: string;}
type User = { id: number; name: string; email: string;};Quasi équivalents. Différences marginales :
interface | type | |
|---|---|---|
| Étendre | extends | & (intersection) |
| Fusion automatique | ✅ (déclarative) | ❌ |
| Unions, primitifs, tuples | ❌ | ✅ |
| Performance | Légèrement mieux pour le compilateur |
Règle pratique : interface pour les objets, type pour les unions, alias, tuples.
Unions et intersections
Section intitulée « Unions et intersections »// Union — A ou Btype ID = string | number;type Status = 'idle' | 'loading' | 'success' | 'error'; // string literal union
function format(id: ID) { if (typeof id === 'string') { // ici TS sait que id est string return id.toUpperCase(); } return id.toFixed(2); // ici number}
// Intersection — A ET Btype Named = { name: string };type Aged = { age: number };type Person = Named & Aged; // { name, age }Discriminated unions — le pattern roi
Section intitulée « Discriminated unions — le pattern roi »type Result<T> = | { ok: true; value: T } | { ok: false; error: Error };
function handle(result: Result<User>) { if (result.ok) { console.log(result.value.name); // TS sait que value existe } else { console.error(result.error); // TS sait que error existe }}Le compilateur affine (narrowing) le type selon le ok — tu ne peux pas faire d’erreur.
Génériques
Section intitulée « Génériques »Une fonction qui marche pour n’importe quel type, sans perdre l’information :
function identity<T>(value: T): T { return value;}
identity('hello'); // T = stringidentity(42); // T = numberidentity({ a: 1 }); // T = { a: number }Génériques contraints
Section intitulée « Génériques contraints »function getProperty<T extends object, K extends keyof T>(obj: T, key: K): T[K] { return obj[key];}
const user = { name: 'Alice', age: 30 };getProperty(user, 'name'); // stringgetProperty(user, 'age'); // numbergetProperty(user, 'email'); // ❌ erreur : 'email' n'existe pas dans TComposants génériques (React)
Section intitulée « Composants génériques (React) »interface ListProps<T> { items: T[]; render: (item: T) => React.ReactNode;}
function List<T>({ items, render }: ListProps<T>) { return <ul>{items.map((it, i) => <li key={i}>{render(it)}</li>)}</ul>;}
<List items={users} render={u => u.name} /> // T = User inféréUtility types — la boîte à outils
Section intitulée « Utility types — la boîte à outils »interface User { id: number; name: string; email: string; password: string;}
// Tous les champs optionnelstype PartialUser = Partial<User>;// { id?, name?, email?, password? }
// Tous requistype RequiredUser = Required<PartialUser>;
// Sélectionner certains champstype PublicUser = Pick<User, 'id' | 'name' | 'email'>;
// Exclure certains champstype NonSensitive = Omit<User, 'password'>;
// Lecture seuletype ReadonlyUser = Readonly<User>;
// Map clé → valeurtype Roles = Record<string, 'admin' | 'user'>;// équivalent à : { [key: string]: 'admin' | 'user' }
// Type de retour d'une fonctiontype Result = ReturnType<typeof loadUsers>;// si loadUsers: () => Promise<User[]>, alors Result = Promise<User[]>
// Awaited — extraire le type d'une Promisetype Users = Awaited<ReturnType<typeof loadUsers>>;// Users = User[]keyof et typeof
Section intitulée « keyof et typeof »const config = { apiUrl: 'https://api.example.com', timeout: 5000, retries: 3,} as const; // ← rend tout readonly et littéral
type Config = typeof config;// { readonly apiUrl: 'https://api.example.com'; readonly timeout: 5000; ... }
type ConfigKey = keyof typeof config;// 'apiUrl' | 'timeout' | 'retries'Narrowing — affiner le type
Section intitulée « Narrowing — affiner le type »function describe(value: unknown) { if (typeof value === 'string') { value.toUpperCase(); // string ici } else if (Array.isArray(value)) { value.length; // any[] ici } else if (value instanceof Date) { value.toISOString(); // Date ici } else if (value && typeof value === 'object' && 'name' in value) { // { name: unknown } ici }}Type guards personnalisés
Section intitulée « Type guards personnalisés »function isUser(v: unknown): v is User { return typeof v === 'object' && v !== null && 'id' in v && 'email' in v;}
const data: unknown = await response.json();if (isUser(data)) { console.log(data.email); // ✅ TS sait que data est User}v is User est une type predicate : la valeur de retour de la fonction renseigne le compilateur.
satisfies — typer sans perdre l’inférence
Section intitulée « satisfies — typer sans perdre l’inférence »Opérateur introduit en TS 4.9 (2022), devenu standard. Résout un problème courant : tu veux contraindre un objet à un type sans perdre l’inférence des littéraux.
Le problème
Section intitulée « Le problème »type Palette = Record<string, string | [number, number, number]>;
// ❌ Avec annotation classique : tu perds le type littéralconst palette: Palette = { red: [255, 0, 0], green: '#00ff00',};
palette.red.toUpperCase();// ↑ type est `string | [number, number, number]` → erreur :// "Property 'toUpperCase' does not exist on type 'string | number[]'"L’annotation : Palette élargit le type à l’union — TS oublie que red est un tuple et green une string.
La solution satisfies
Section intitulée « La solution satisfies »// ✅ Avec satisfies : contrainte vérifiée + types littéraux préservésconst palette = { red: [255, 0, 0], green: '#00ff00',} satisfies Palette;
palette.green.toUpperCase(); // OK — TS sait que green est une stringpalette.red[0]; // OK — TS sait que red est un tuplesatisfies vérifie que la valeur correspond au type, mais garde le type le plus précis (inféré). Tu obtiens validation et précision.
Cas d’usage typiques
Section intitulée « Cas d’usage typiques »// 1. Configuration avec valeurs hétérogènesconst routes = { home: { path: '/', exact: true }, user: { path: '/user/:id', exact: false },} satisfies Record<string, RouteConfig>;
routes.home.exact; // boolean ✅ (vs `boolean` perdu sans satisfies)
// 2. Map de constantes typéesconst STATUS = { PENDING: 'pending', ACTIVE: 'active', ARCHIVED: 'archived',} as const satisfies Record<string, string>;
type Status = typeof STATUS[keyof typeof STATUS];// → 'pending' | 'active' | 'archived' (pas juste `string`)
// 3. Validation de schéma sans perdre les clésconst schema = { name: z.string(), age: z.number(),} satisfies Record<string, z.ZodSchema>;// schema.name typé `ZodString`, pas `ZodSchema` génériquesatisfies vs as vs annotation :
Section intitulée « satisfies vs as vs annotation : »| Syntaxe | Vérifie ? | Garde inférence ? | Quand |
|---|---|---|---|
const x: T = ... | ✅ | ❌ (élargit à T) | Tu veux le type abstrait T partout |
const x = ... as T | ❌ (assertion non vérifiée, dangereux) | ❌ | Sortie d’un unknown, dernier recours |
const x = ... satisfies T | ✅ | ✅ | Le défaut moderne pour les configs/constantes |
Règle 2026 : utilise satisfies quand tu écris une valeur littérale que tu veux contraindre. Réserve l’annotation : aux paramètres de fonction et aux types de retour.
Types branchés (branded types)
Section intitulée « Types branchés (branded types) »Pour distinguer 2 types qui ont la même forme structurelle :
type UserId = string & { __brand: 'UserId' };type ProductId = string & { __brand: 'ProductId' };
function findUser(id: UserId) { ... }
const userId = 'u123' as UserId;const productId = 'p456' as ProductId;
findUser(userId); // ✅findUser(productId); // ❌ erreur même si c'est techniquement une stringPermet d’éviter qu’on passe un ID de produit à une fonction qui attend un ID utilisateur.
any vs unknown
Section intitulée « any vs unknown »const a: any = 'hello';a.toUpperCase(); // pas d'erreur, mais aucun contrôlea.foo.bar.baz; // pas d'erreur ! 💥 catastrophe en runtime
const b: unknown = 'hello';b.toUpperCase(); // ❌ erreur : il faut narrowing avantif (typeof b === 'string') { b.toUpperCase(); // ✅}Règle : any est interdit. Utilise unknown quand tu ne sais pas le type — il te force à valider avant.
Validation runtime — Zod
Section intitulée « Validation runtime — Zod »TypeScript valide à la compilation. Au runtime, les types sont effacés. Pour valider une réponse API, un input utilisateur, etc., utilise une lib :
import { z } from 'zod';
const UserSchema = z.object({ id: z.number(), name: z.string().min(1), email: z.string().email(),});
type User = z.infer<typeof UserSchema>; // type extrait du schéma
const data = await fetch('/api/me').then(r => r.json());const user = UserSchema.parse(data); // throw si invalide// user est typé User automatiquementC’est le combo gagnant 2026 : Zod + TypeScript = un seul schéma pour valider et typer.
Erreurs courantes et solutions
Section intitulée « Erreurs courantes et solutions »”Object is possibly ‘undefined’"
Section intitulée « ”Object is possibly ‘undefined’" »const user = users.find(u => u.id === 1);console.log(user.name); // ❌ user peut être undefined
// Solutionsconsole.log(user?.name);if (user) console.log(user.name);"Argument of type ‘X’ is not assignable to type ‘Y’"
Section intitulée « "Argument of type ‘X’ is not assignable to type ‘Y’" »// ❌const status: 'success' | 'error' = response.status; // si type est plus large
// ✅ assertion expliciteconst status = response.status as 'success' | 'error';
// ✅ mieux : valider d'abordif (response.status === 'success' || response.status === 'error') { const status = response.status; // narrowing}"Property ‘X’ does not exist on type ‘Y’”
Section intitulée « "Property ‘X’ does not exist on type ‘Y’” »Souvent : tu as une union et il faut narrower :
type Pet = { type: 'cat'; lives: number } | { type: 'dog'; barks: boolean };
function info(pet: Pet) { if (pet.type === 'cat') { console.log(pet.lives); // ✅ } else { console.log(pet.barks); // ✅ }}Auto-évaluation
Section intitulée « Auto-évaluation »Pour aller plus loin
Section intitulée « Pour aller plus loin »- TypeScript Handbook (officiel) : typescriptlang.org/docs/handbook
- Total TypeScript — Matt Pocock (gratuit en partie, payant pour le reste)
- Type Challenges — github.com/type-challenges/type-challenges
- Zod — zod.dev
🪤 Pièges réels rencontrés
Section intitulée « 🪤 Pièges réels rencontrés »Piège réel rencontré — Zod 4 incompatible avec @hono/zod-validator Dépendances
🩹 Symptôme
npm error code ERESOLVEpeer zod@"^3.19.1" from @hono/zod-validator@0.4.3Found: zod@4.4.1🔍 Cause
Zod 4 a introduit des breaking changes dans son API interne. Beaucoup de libs de l’écosystème (validators, ORM intégrations) ont des peer dependencies fixées sur Zod 3 et n’ont pas encore publié de version compatible Zod 4.
🩺 Fix
Deux options :
- Conservateur — downgrade Zod à
^3.23.8danspackage.json: fonctionne avec tout l’écosystème actuel. - Bleeding edge — vérifier si
@hono/zod-validator@0.5+est sorti, qui supporte Zod 4. Idem pour les autres libs concernées.
🧠 Leçon
Avant d’upgrader une dépendance « cœur » (Zod, React, TypeScript, ORM), faire npm ls ou npm-check-updates --doctor pour repérer les peer-deps qui ne suivront pas. Une migration majeure ne se fait jamais seule — elle entraîne tout son écosystème.
🔍 Tous les pièges du guide sont sur la page /pieges/ — searchable par symptôme.
Fin de l’axe 6. Direction l’axe 7 — Frameworks frontend, ou attaque le projet « SPA TypeScript » dans exercises/06-javascript-typescript/.