TypeScript : typer son JavaScript
Annotations, interfaces, unions, génériques, null safety et configuration. Ajoute une couche de types à JavaScript pour attraper les bugs avant l'exécution.
À la fin du cours, tu sais
- Comprendre ce que TypeScript ajoute à JavaScript et comment il se compile
- Annoter des variables et laisser l'inférence travailler pour toi
- Modéliser des données avec les interfaces et les types
- Combiner des types avec les unions et les types littéraux
- Typer précisément les fonctions (paramètres, retour, optionnels)
- Écrire du code réutilisable avec les génériques
- Éliminer les erreurs de null avec la null safety de TypeScript
Prérequis
- Maîtriser les bases de JavaScript (voir le cours « Les bases de JavaScript »)
- Node.js installé pour compiler, ou l'éditeur en ligne du site officiel de TypeScript
Étape 1 sur 11 : Pourquoi TypeScript
Chapitre 1
Pourquoi TypeScript
JavaScript te laisse écrire n'importe quoi et ne se plaint qu'à l'exécution. TypeScript ajoute une vérification des types qui attrape ces erreurs bien plus tôt.
En JavaScript, une variable peut contenir n'importe quel type de valeur, et ce type peut même changer en cours de route. C'est souple, mais c'est aussi une source de bugs : rien ne t'empêche d'appeler .toUpperCase() sur un nombre, ou de passer une chaîne là où on attendait un objet. L'erreur ne se révèle qu'au moment où le code s'exécute, souvent en production, devant l'utilisateur·ice. TypeScript répond à ce problème : c'est un sur-ensemble de JavaScript qui ajoute un système de types vérifiés avant l'exécution.
« Sur-ensemble » (superset) signifie que tout code JavaScript valide est déjà du TypeScript valide : tu ne repars pas de zéro, tu ajoutes progressivement des annotations de type à du JavaScript que tu connais déjà. TypeScript ne s'exécute pas directement dans le navigateur : il faut le compiler (on dit aussi transpiler) en JavaScript ordinaire avec le compilateur tsc. C'est pendant cette compilation que TypeScript vérifie tes types et signale les incohérences.
Le même code, avec et sans types
// JavaScript : rien ne signale l'erreur avant l'exécution
function double(n) {
return n * 2;
}
double("bonjour"); // NaN à l'exécution, aucune alerte avant
// TypeScript : l'erreur est signalée dans l'éditeur, immédiatement
function doubleTS(n: number): number {
return n * 2;
}
doubleTS("bonjour"); // ERREUR : argument de type 'string' non assignableDans la version typée, n: number annonce que le paramètre doit être un nombre, et : number après les parenthèses annonce le type de retour. Si quelqu'un appelle la fonction avec une chaîne, TypeScript refuse de compiler et souligne l'erreur directement dans l'éditeur, avant même d'exécuter quoi que ce soit. Tu passes d'un bug découvert en production à un bug attrapé pendant que tu écris.
Compiler un fichier TypeScript
# Installer TypeScript globalement (une fois)
npm install -g typescript
# Compiler un fichier .ts en .js
tsc script.ts # produit script.js
# Vérifier les types sans générer de fichiers
tsc --noEmit # utile en intégration continuePourquoi toute l'industrie a adopté TypeScript
Vrai ou faux ?
Le code TypeScript s'exécute directement dans le navigateur, sans étape intermédiaire.
Chapitre 2
Les types de base et l'inférence
Annoter une variable est simple, mais TypeScript devine souvent le type tout seul. Savoir quand annoter et quand laisser faire est la première compétence à acquérir.
Une annotation de type s'écrit avec deux-points suivis du type : const age: number = 30. Les types de base reprennent les valeurs de JavaScript : string pour le texte, number pour les nombres, boolean pour les booléens. Pour un tableau, on précise le type de ses éléments avec des crochets : string[] se lit « un tableau de chaînes ».
const prenom: string = "Alice";
const age: number = 30;
const actif: boolean = true;
// Un tableau de chaînes
const fruits: string[] = ["pomme", "banane"];
// Un tableau de nombres
const notes: number[] = [12, 15, 18];L'inférence : laisse TypeScript deviner
Voici la première bonne surprise : dans la plupart des cas, tu n'as pas besoin d'écrire l'annotation. TypeScript infère (déduit) le type à partir de la valeur affectée. Si tu écris const age = 30, TypeScript sait déjà que age est un number, et t'interdira d'y ranger une chaîne plus tard. La règle pragmatique : laisse l'inférence faire quand le type est évident, et annote seulement quand ce n'est pas le cas (les paramètres de fonction, notamment, ne sont jamais inférés).
// Inutile d'annoter : TypeScript infère 'number'
const age = 30;
// age = "trente"; // ERREUR quand même : le type est verrouillé
// Annotation utile ici : le paramètre n'est jamais inféré
function saluer(prenom: string) {
return "Bonjour " + prenom;
}Survole une variable dans ton éditeur
Deux types à connaître : any et void
Le type any désactive toute vérification : une valeur any peut être n'importe quoi, et TypeScript te laisse tout faire dessus. C'est une porte de sortie tentante mais dangereuse, on y revient au chapitre 7. Le type void, lui, désigne l'absence de valeur de retour : c'est le type d'une fonction qui agit sans rien renvoyer (comme console.log).
Vrai ou faux ?
En TypeScript, il faut annoter explicitement le type de chaque variable, sinon le code ne compile pas.
Chapitre 3
Interfaces et types : modéliser des objets
Le vrai intérêt de TypeScript commence quand on décrit la forme des objets qu'on manipule. C'est le rôle des interfaces et des alias de type.
En JavaScript, un objet peut avoir n'importe quelle forme, et rien ne garantit qu'il contienne bien les propriétés attendues. TypeScript te permet de décrire la forme d'un objet une fois pour toutes, puis de l'imposer partout. Deux outils font ça, avec une syntaxe légèrement différente : l'interface et l'alias de type (type).
Décrire un objet avec une interface
interface Utilisateur {
prenom: string;
age: number;
email: string;
actif: boolean;
}
// Cet objet DOIT respecter la forme de l'interface
const alice: Utilisateur = {
prenom: "Alice",
age: 30,
email: "alice@exemple.fr",
actif: true,
};
// Oublier une propriété ou se tromper de type = erreur de compilationUne fois l'interface Utilisateur définie, TypeScript vérifie que chaque objet annoté : Utilisateur possède toutes les propriétés, avec les bons types. Oublier email ou mettre age: "trente" devient une erreur signalée dans l'éditeur. Mieux : partout où tu manipules un Utilisateur, l'éditeur t'autocomplète ses propriétés. L'interface devient une documentation vivante de tes données.
Propriétés optionnelles et en lecture seule
interface Produit {
readonly id: number; // readonly : ne peut plus être modifié après création
nom: string;
prix: number;
promo?: number; // le ? rend la propriété optionnelle
}
const t: Produit = { id: 1, nom: "T-shirt", prix: 20 }; // promo omis : OK
// t.id = 2; // ERREUR : id est en lecture seuleLe point d'interrogation (promo?) rend une propriété optionnelle : elle peut être présente ou absente. Le mot-clé readonly interdit de modifier une propriété après la création de l'objet, utile pour un identifiant qui ne doit jamais changer. Ces deux marqueurs rendent tes intentions explicites et empêchent des modifications accidentelles.
interface ou type : lequel choisir ?
// Un alias de type fait presque la même chose
type Point = {
x: number;
y: number;
};
const origine: Point = { x: 0, y: 0 };L'alias type et l'interface se recouvrent largement pour décrire des objets. La convention courante : utilise interface pour décrire la forme d'un objet (surtout s'il peut être étendu), et type pour les cas que interface ne couvre pas, comme les unions qu'on voit au chapitre suivant. Pour débuter, retiens simplement que les deux existent et font le job pour un objet ; le choix précis viendra avec l'expérience.
Vrai ou faux ?
Une propriété marquée avec ? dans une interface est obligatoire.
Chapitre 4
Unions, littéraux et narrowing
Une valeur qui peut être « soit ceci, soit cela » se modélise avec une union. Combinée aux types littéraux, c'est l'une des idées les plus puissantes de TypeScript.
Souvent, une valeur peut avoir plusieurs types possibles : un identifiant qui est soit un number, soit une string ; une réponse qui est soit une donnée, soit null. TypeScript exprime ça avec un type union, formé avec la barre verticale | (« ou »). string | number se lit « une chaîne ou un nombre ».
// Une union : la valeur est soit un number, soit une string
let identifiant: string | number;
identifiant = 42; // OK
identifiant = "AB-123"; // OK aussi
// identifiant = true; // ERREUR : boolean n'est pas dans l'unionLes types littéraux : des valeurs précises
Encore plus fort : un type peut être une valeur exacte, pas seulement une catégorie. "actif" peut être un type à part entière, qui n'accepte que la chaîne "actif". Combinés à une union, les types littéraux décrivent un ensemble fermé de valeurs autorisées, façon liste de choix.
// L'union de littéraux modélise un ensemble fini de valeurs
type Statut = "brouillon" | "publié" | "archivé";
function changerStatut(s: Statut) {
console.log("Nouveau statut : " + s);
}
changerStatut("publié"); // OK
// changerStatut("supprimé"); // ERREUR : "supprimé" n'est pas un StatutCe type Statut vaut de l'or : impossible de passer une valeur non prévue, et l'éditeur autocomplète les trois valeurs valides. C'est bien plus sûr qu'une simple string, où n'importe quelle faute de frappe passerait inaperçue. Tu retrouveras ce motif partout, notamment pour typer les variantes d'un composant en React.
Le narrowing : réduire le type selon le contexte
Quand une valeur est une union, TypeScript t'oblige à vérifier son type réel avant de faire une opération spécifique : c'est le narrowing (« rétrécissement »). En testant le type avec typeof, TypeScript comprend, à l'intérieur du bloc, que la valeur est du type vérifié, et t'autorise les opérations correspondantes.
function afficher(valeur: string | number) {
if (typeof valeur === "string") {
// Ici, TypeScript SAIT que valeur est une string
console.log(valeur.toUpperCase());
} else {
// Ici, TypeScript SAIT que valeur est un number
console.log(valeur.toFixed(2));
}
}Le narrowing te protège des vraies erreurs
typeof, appeler valeur.toUpperCase() planterait si valeur est un nombre. TypeScript refuse de compiler tant que tu n'as pas prouvé le type. Ce n'est pas une contrainte gratuite : c'est exactement le genre de bug qui, en JavaScript pur, ne se voit qu'en production.Vrai ou faux ?
Le type type Couleur = "rouge" | "vert" | "bleu" accepte n'importe quelle chaîne de caractères.
Chapitre 5
Les fonctions typées
Typer une fonction, c'est documenter son contrat : ce qu'elle attend en entrée et ce qu'elle renvoie. TypeScript vérifie que le contrat est respecté des deux côtés.
Une fonction bien typée est une fonction qu'on peut appeler en confiance. On annote chaque paramètre et le type de retour. Le type de retour peut souvent être inféré, mais l'écrire explicitement est une bonne habitude : ça documente l'intention et attrape les erreurs où tu renvoies par mégarde le mauvais type.
// Paramètres typés + type de retour explicite
function additionner(a: number, b: number): number {
return a + b;
}
// Fonction fléchée typée (courant en React)
const multiplier = (a: number, b: number): number => a * b;
// void : ne renvoie rien, agit seulement
function journaliser(message: string): void {
console.log(message);
}Paramètres optionnels et valeurs par défaut
// Le ? rend un paramètre optionnel (il vaut undefined si omis)
function saluer(prenom: string, titre?: string): string {
if (titre) {
return `Bonjour ${titre} ${prenom}`;
}
return `Bonjour ${prenom}`;
}
saluer("Alice"); // "Bonjour Alice"
saluer("Alice", "Dr"); // "Bonjour Dr Alice"
// Valeur par défaut : le type est inféré depuis la valeur
function incrementer(n: number, pas = 1): number {
return n + pas;
}Un paramètre suivi d'un ? est optionnel : s'il est omis, il vaut undefined, et TypeScript t'oblige à gérer ce cas (ici le if (titre)). Un paramètre avec une valeur par défaut (pas = 1) prend cette valeur si l'argument n'est pas fourni ; son type est alors inféré, inutile de l'annoter. Ces deux mécanismes rendent tes fonctions souples sans sacrifier la sécurité des types.
Typer une fonction passée en argument
// Le type d'une fonction : (paramètres) => typeDeRetour
function appliquer(valeur: number, operation: (n: number) => number): number {
return operation(valeur);
}
appliquer(5, (n) => n * 2); // 10
appliquer(5, (n) => n + 100); // 105Quand une fonction reçoit une autre fonction en argument (un motif omniprésent avec map, filter ou les gestionnaires d'événements), on type cette fonction avec la syntaxe (paramètres) => typeDeRetour. Ici operation: (n: number) => number annonce « une fonction qui prend un nombre et renvoie un nombre ». TypeScript vérifiera que la fonction passée respecte bien cette signature.
Vrai ou faux ?
Le type de retour void signifie qu'une fonction renvoie la valeur null.
Chapitre 6
Les génériques
Comment écrire une fonction qui marche avec n'importe quel type, sans perdre la sécurité des types ? Les génériques sont la réponse, et ils sont partout.
Imagine une fonction qui renvoie le premier élément d'un tableau. Elle devrait marcher pour un tableau de nombres, de chaînes, d'objets. Tu pourrais la typer avec any, mais tu perdrais toute information sur le type renvoyé. Les génériques résolvent exactement ça : ils permettent d'écrire du code qui fonctionne avec un type que l'on ne connaît pas encore, mais en le conservant. On utilise une lettre entre chevrons, par convention T (pour Type).
// T est un type "à trou", déterminé à l'appel
function premier<T>(tableau: T[]): T {
return tableau[0];
}
const n = premier([1, 2, 3]); // T = number, n est un number
const s = premier(["a", "b", "c"]); // T = string, s est un string
// TypeScript connaît le type de retour précis à chaque appelLe <T> après le nom de la fonction déclare un paramètre de type. Quand tu appelles premier([1, 2, 3]), TypeScript déduit que T vaut number, et sait donc que le résultat est un number. Avec un tableau de chaînes, T devient string. Une seule fonction, tous les types, et la sécurité conservée à chaque appel : c'est toute la magie.
Tu utilises déjà des génériques
Bonne nouvelle : tu manipules des génériques sans le savoir. Array<string> est la forme longue de string[] ; Array est un type générique. Une Promise<Utilisateur> est une promesse qui, une fois résolue, contient un Utilisateur. Ces chevrons que tu vois partout sont des génériques qui précisent « de quoi » est fait le conteneur.
const noms: Array<string> = ["Alice", "Bob"]; // = string[]
// Une fonction asynchrone renvoie une Promise<T>
async function chargerUtilisateur(): Promise<Utilisateur> {
const reponse = await fetch("/api/user");
return reponse.json();
}
interface Utilisateur {
prenom: string;
age: number;
}Contraindre un générique
// T extends { length: number } : T doit avoir une propriété length
function estVide<T extends { length: number }>(valeur: T): boolean {
return valeur.length === 0;
}
estVide(""); // OK, une string a une length
estVide([1, 2, 3]); // OK, un tableau aussi
// estVide(42); // ERREUR : un number n'a pas de lengthNe force pas les génériques trop tôt
useState<string>() en React ou Promise<Data>, tu comprends maintenant que les chevrons précisent le type contenu. C'est déjà 90 % de ce dont tu as besoin comme junior.Vrai ou faux ?
Un générique <T> oblige à choisir un type fixe une fois pour toutes, valable pour tous les appels de la fonction.
Chapitre 7
any, unknown et null safety
Les valeurs manquantes (null, undefined) sont la première cause de plantage en JavaScript. TypeScript t'oblige à les gérer, à condition de ne pas tricher avec any.
Le célèbre « Cannot read property of undefined » a fait planter des millions de pages. TypeScript s'attaque frontalement à ce problème avec la null safety : quand une valeur peut être null ou undefined, il t'oblige à vérifier avant de l'utiliser. Mais ce filet de sécurité peut être troué par un mauvais usage de any. Ce chapitre traite des deux ensemble.
any vs unknown
any désactive complètement la vérification : une valeur any peut tout faire, TypeScript ferme les yeux. C'est une trappe qui annule tout l'intérêt de TypeScript, à éviter autant que possible. Quand tu as vraiment une valeur de type inconnu (une réponse d'API non typée, par exemple), préfère unknown : lui aussi accepte n'importe quoi, mais il t'oblige à vérifier le type avant de l'utiliser. unknown, c'est un any honnête.
let dangereux: any = "texte";
dangereux.n_importe_quoi(); // aucune erreur signalée... jusqu'au plantage
let prudent: unknown = "texte";
// prudent.toUpperCase(); // ERREUR : type inconnu, vérifie d'abord
if (typeof prudent === "string") {
prudent.toUpperCase(); // OK, le narrowing a prouvé le type
}strictNullChecks : gérer null et undefined
Avec l'option strictNullChecks activée (elle l'est dans tout projet sérieux), TypeScript considère null et undefined comme des types à part. Si une valeur peut être absente, son type le dit : string | undefined. Impossible alors d'appeler une méthode dessus sans avoir écarté le cas undefined. TypeScript transforme une erreur d'exécution en erreur de compilation.
// find peut ne rien trouver : le résultat est Utilisateur | undefined
const users: Utilisateur[] = [];
const trouve = users.find((u) => u.prenom === "Alice");
// trouve.age; // ERREUR : trouve est peut-être undefined
// L'optional chaining : lit la propriété seulement si trouve existe
console.log(trouve?.age);
// Le nullish coalescing : une valeur de repli si null/undefined
const age = trouve?.age ?? 0; // age vaut trouve.age, ou 0 si absent
interface Utilisateur {
prenom: string;
age: number;
}Deux opérateurs facilitent la vie. Le chaînage optionnel ?. lit une propriété seulement si l'objet existe, sinon renvoie undefined au lieu de planter. Le coalescing des nuls ?? fournit une valeur de repli quand la partie gauche est null ou undefined. Ensemble, trouve?.age ?? 0 se lit « l'âge s'il existe, sinon 0 ». C'est l'idiome standard pour manipuler des données potentiellement absentes.
any est contagieux
any, tout ce qui en découle perd aussi ses types, sans que tu t'en rendes compte. Un projet qui laisse filer les any se retrouve avec la fausse impression d'être typé alors qu'il ne l'est plus. La règle de rigueur, celle qu'un jury RNCP apprécie : bannir any, utiliser unknown quand le type est réellement inconnu.🧭 Ta réponse d'API plante avec « Cannot read property of undefined »
Tu récupères un utilisateur depuis une API et tu affiches <code>user.profil.ville</code>, mais ça plante en production sur certains comptes. Comment sécurises-tu ça avec TypeScript ?
TypeScript ne t'avait pas alerté·e parce que la réponse est typée any. Quelle est ta première décision ?
Pour aller plus loin
- Le manuel officiel de TypeScript (en anglais, la référence) · typescriptlang.org (en anglais) (nouvel onglet)
- TypeScript sur MDN, en français · MDN (nouvel onglet)
- L'éditeur en ligne pour essayer TypeScript sans rien installer · TS Playground (en anglais) (nouvel onglet)
- Enchaîne avec : React, composants et état · Ce site (nouvel onglet)
Chapitre 8
TypeScript au quotidien
Configurer le compilateur, utiliser les types utilitaires, adopter les bons réflexes : ce qui distingue un code TypeScript amateur d'un code professionnel.
Un projet TypeScript se configure avec un fichier tsconfig.json à la racine. Il indique au compilateur quelle version de JavaScript produire, quels fichiers compiler, et surtout quel niveau de rigueur appliquer. La règle numéro un : active le mode strict. Il regroupe toutes les vérifications importantes (dont strictNullChecks) et empêche les any implicites.
{
"compilerOptions": {
"target": "ES2022",
"strict": true,
"noUnusedLocals": true,
"noImplicitAny": true
}
}Les types utilitaires : ne te répète pas
TypeScript fournit des types utilitaires qui transforment un type existant, pour t'éviter de réécrire des variantes à la main. Les plus utiles au quotidien : Partial<T> rend toutes les propriétés optionnelles, Pick<T, ...> garde seulement certaines propriétés, Omit<T, ...> en retire certaines.
interface Utilisateur {
id: number;
prenom: string;
email: string;
}
// Toutes les propriétés deviennent optionnelles (utile pour une mise à jour)
type MajUtilisateur = Partial<Utilisateur>;
// { id?: number; prenom?: string; email?: string }
// On garde seulement id et prenom
type Apercu = Pick<Utilisateur, "id" | "prenom">;
// On retire id (par exemple pour un formulaire de création)
type NouvelUtilisateur = Omit<Utilisateur, "id">;Ces utilitaires évitent la duplication : au lieu de définir trois interfaces qui se ressemblent, tu dérives des variantes d'un type de base. Le jour où tu ajoutes une propriété à Utilisateur, tous les types dérivés se mettent à jour automatiquement. C'est un gain de robustesse considérable sur un vrai projet.
Les réflexes à adopter
- Active
strictdès le premier jour : c'est bien plus dur à ajouter après coup. - Bannis
any: utiliseunknownpuis un narrowing quand le type est vraiment inconnu. - Laisse l'inférence travailler pour les variables, annote les paramètres et les retours de fonction.
- Modélise tes données avec des interfaces, une source de vérité pour toute l'application.
- Gère l'absence avec
?.et??plutôt que de forcer avec!.
Le socle de tout le développement moderne
Vrai ou faux ?
Le type utilitaire Partial<Utilisateur> rend toutes les propriétés de l'interface optionnelles.
🛠️ Exercice optionnel
Typer un mini panier d'achats
Tu vas modéliser et typer un petit panier : une interface pour les articles, une union de littéraux pour l'état d'une commande, des fonctions typées pour manipuler tout ça, et la gestion propre d'un article éventuellement absent. Tout tient dans un seul fichier .ts.
Ta mission
- Interface
Article:id(number, enreadonly),nom(string),prix(number), etpromo(number, optionnel). - Type
Statut: une union de littéraux"panier" | "commandée" | "livrée". - Une fonction
prixFinal(article: Article): numberqui renvoie le prix, réduit du pourcentagepromos'il existe (sinon le prix plein). - Une fonction
total(articles: Article[]): numberqui additionne les prix finaux (avecreduceou une boucle). - Une fonction
trouverArticle(articles: Article[], id: number): Article | undefined, et affiche son nom en gérant le cas « non trouvé » avec?.et??. - Bonus : un type
NouvelArticle = Omit<Article, "id">pour un formulaire de création.
Tu bloques ? Des indices, à dévoiler quand tu en as besoin.
Indice 1
Indice masqué.
Indice 2
Indice masqué.
Indice 3
Indice masqué.
✅ QCM de fin de cours
Teste tes acquis
10 questions, plusieurs réponses parfois possibles. Coche tout ce qui te semble juste, puis valide pour voir ton score et les explications.
- 1
Quelle affirmation décrit correctement TypeScript ?
- 2
Dans
const age = 30sans annotation, quel est le type deage? - 3
Que fait le
?dansinterface Produit { promo?: number }? - 4
Que représente le type
string | number? - 5
Que permet le narrowing avec
if (typeof valeur === "string")? - 6
Quelle est la différence entre
anyetunknown? - 7
Dans
function premier<T>(t: T[]): T, que désigneT? - 8
Que renvoie
utilisateur?.emailsiutilisateurvautundefined? - 9
Que fait l'opérateur
??dansage ?? 0? - 10
À quoi sert le type utilitaire
Omit<Utilisateur, "id">?
Tu peux laisser des questions sans réponse, elles compteront comme fausses.
🎓 Attestation
Ton attestation de réussite
Termine le QCM avec au moins 80% de bonnes réponses pour débloquer ton attestation de réussite.
Tu veux ce cours pour ton équipe ?
Je peux adapter et animer ce cours pour tes formateur·ices ou tes apprenant·es, en présentiel ou en distanciel. Parlons-en pendant l'audit gratuit.
Réserver un audit gratuit →