Esc
 Naviguer  Ouvrir Esc Fermer
Aller au contenu

6.4 — TypeScript

Débutant 30 min prérequis : axes 6.1 à 6.3 lus

🎯 Objectif : pouvoir typer correctement une vraie application — fonctions, données API, formulaires, hooks — sans any et 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

3 raisons concrètes :

  1. Bugs détectés à la compilation : tu sais que user.email existe avant de pousser en prod.
  2. Auto-complétion magique : ton IDE devient ton meilleur ami.
  3. 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).

Fenêtre de terminal
npm init -y
npm install -D typescript tsx @types/node
npx tsc --init

Un 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).

// Variables
const name: string = 'Alice';
const age: number = 30;
const active: boolean = true;
const tags: string[] = ['js', 'web'];
const tuple: [string, number] = ['Alice', 30];
// Fonctions
function add(a: number, b: number): number {
return a + b;
}
const double = (x: number): number => x * 2;
// Optionnel et défaut
function 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 User {
id: number;
name: string;
email: string;
}
type User = {
id: number;
name: string;
email: string;
};

Quasi équivalents. Différences marginales :

interfacetype
Étendreextends& (intersection)
Fusion automatique✅ (déclarative)
Unions, primitifs, tuples
PerformanceLégèrement mieux pour le compilateur

Règle pratique : interface pour les objets, type pour les unions, alias, tuples.

// Union — A ou B
type 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 B
type Named = { name: string };
type Aged = { age: number };
type Person = Named & Aged; // { name, age }
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.

Une fonction qui marche pour n’importe quel type, sans perdre l’information :

function identity<T>(value: T): T {
return value;
}
identity('hello'); // T = string
identity(42); // T = number
identity({ a: 1 }); // T = { a: number }
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'); // string
getProperty(user, 'age'); // number
getProperty(user, 'email'); // ❌ erreur : 'email' n'existe pas dans T
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é
interface User {
id: number;
name: string;
email: string;
password: string;
}
// Tous les champs optionnels
type PartialUser = Partial<User>;
// { id?, name?, email?, password? }
// Tous requis
type RequiredUser = Required<PartialUser>;
// Sélectionner certains champs
type PublicUser = Pick<User, 'id' | 'name' | 'email'>;
// Exclure certains champs
type NonSensitive = Omit<User, 'password'>;
// Lecture seule
type ReadonlyUser = Readonly<User>;
// Map clé → valeur
type Roles = Record<string, 'admin' | 'user'>;
// équivalent à : { [key: string]: 'admin' | 'user' }
// Type de retour d'une fonction
type Result = ReturnType<typeof loadUsers>;
// si loadUsers: () => Promise<User[]>, alors Result = Promise<User[]>
// Awaited — extraire le type d'une Promise
type Users = Awaited<ReturnType<typeof loadUsers>>;
// Users = User[]
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'
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
}
}
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.

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.

type Palette = Record<string, string | [number, number, number]>;
// ❌ Avec annotation classique : tu perds le type littéral
const 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.

// ✅ Avec satisfies : contrainte vérifiée + types littéraux préservés
const palette = {
red: [255, 0, 0],
green: '#00ff00',
} satisfies Palette;
palette.green.toUpperCase(); // OK — TS sait que green est une string
palette.red[0]; // OK — TS sait que red est un tuple

satisfies vérifie que la valeur correspond au type, mais garde le type le plus précis (inféré). Tu obtiens validation et précision.

// 1. Configuration avec valeurs hétérogènes
const 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ées
const 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és
const schema = {
name: z.string(),
age: z.number(),
} satisfies Record<string, z.ZodSchema>;
// schema.name typé `ZodString`, pas `ZodSchema` générique
SyntaxeVé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 TLe 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.

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 string

Permet d’éviter qu’on passe un ID de produit à une fonction qui attend un ID utilisateur.

const a: any = 'hello';
a.toUpperCase(); // pas d'erreur, mais aucun contrôle
a.foo.bar.baz; // pas d'erreur ! 💥 catastrophe en runtime
const b: unknown = 'hello';
b.toUpperCase(); // ❌ erreur : il faut narrowing avant
if (typeof b === 'string') {
b.toUpperCase(); // ✅
}

Règle : any est interdit. Utilise unknown quand tu ne sais pas le type — il te force à valider avant.

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 automatiquement

C’est le combo gagnant 2026 : Zod + TypeScript = un seul schéma pour valider et typer.

const user = users.find(u => u.id === 1);
console.log(user.name); // ❌ user peut être undefined
// Solutions
console.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 explicite
const status = response.status as 'success' | 'error';
// ✅ mieux : valider d'abord
if (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); // ✅
}
}
Tu reçois `data: unknown` depuis fetch().json(). Tu fais `data.name`. Que dit TypeScript ?
Tu veux Pick<User, 'id' | 'name'>. Quel résultat ?
Quelle utilité de `as const` sur un objet de config ?
Piège réel rencontré — Zod 4 incompatible avec @hono/zod-validator Dépendances

🩹 Symptôme

npm error code ERESOLVE
peer zod@"^3.19.1" from @hono/zod-validator@0.4.3
Found: 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.8 dans package.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/.