Qu’est-ce que TypeScript ?
TypeScript est un superset de JavaScript à typage statique qui ajoute des annotations de type optionnelles vérifiées lors de la compilation. Il est compilé en JavaScript pur, ce qui lui permet de s’exécuter partout où JavaScript fonctionne : dans les navigateurs, Node.js, Deno et Bun.
Créé par Microsoft en 2012, TypeScript est devenu le choix par défaut pour les projets JavaScript sérieux. La raison est simple : il détecte les bugs avant même que votre code ne soit exécuté. Une faute de frappe dans le nom d’une propriété, un argument manquant, un type de retour incorrect — TypeScript signale ces erreurs à la compilation au lieu de les laisser provoquer des plantages en production.
La meilleure façon de concevoir TypeScript est de le voir comme du JavaScript avec un filet de sécurité. Vous écrivez le même langage que vous connaissez déjà, mais vous pouvez décrire la structure de vos données et les contrats de vos fonctions. Le compilateur vérifie que tout concorde, puis supprime les types pour ne laisser que du JavaScript propre.
Pourquoi TypeScript est essentiel
JavaScript est un langage à typage dynamique, ce qui signifie que vous pouvez passer n’importe quelle valeur à n’importe quelle fonction, et l’erreur n’apparaît que lorsque le code est réellement exécuté. Pour un petit script, cela ne pose pas de problème. Mais dans une application d’envergure avec des dizaines de développeurs, c’est la recette idéale pour générer des bugs en production.
TypeScript résout ce problème en vérifiant les types avant l’exécution du code :
- Détection des bugs à la compilation — une faute de frappe dans une propriété, un mauvais type d’argument ou l’oubli de la gestion d’une valeur null, et TypeScript vous avertit immédiatement.
- Meilleure expérience dans l’éditeur — l’autocomplétion fonctionne car l’éditeur connaît les propriétés d’un objet. Le refactoring est sécurisé car le compilateur identifie chaque endroit nécessitant une mise à jour.
- Code auto-documenté — les signatures des fonctions décrivent exactement ce qu’elles attendent et ce qu’elles retournent. La lecture des types vous indique comment utiliser le code sans avoir à en lire l’implémentation.
- Refactoring plus sûr — modifiez un type et le compilateur mettra en évidence tous les endroits qui sont impactés. Plus besoin de fouiller dans la base de code pour traquer des erreurs d’exécution.
- Adoption progressive — vous pouvez ajouter TypeScript à un projet JavaScript existant de manière incrémentale, fichier par fichier.
TypeScript n’ajoute aucun overhead à l’exécution. Les types sont supprimés lors de la compilation, donc le JavaScript livré aux utilisateurs est identique, que vous l’ayez écrit en TypeScript ou non.
Comment fonctionne TypeScript
Le code TypeScript réside dans des fichiers .ts. Le compilateur TypeScript (tsc) lit ces fichiers, vérifie les types et génère des fichiers .js dont les annotations de type ont été supprimées.
// app.ts
function add(a: number, b: number): number {
return a + b;
}
const result = add(1, 2); // result is number
Le compilateur vérifie que a et b sont des nombres, que la fonction retourne un nombre et que result reçoit un nombre. Si un élément ne correspond pas, vous obtenez une erreur à la compilation. Le JavaScript généré est simplement :
function add(a, b) {
return a + b;
}
const result = add(1, 2);
Pas de types, pas de surcharge. La sécurité n’intervient que lors de l’étape de build.
Annotations de type
Les annotations de type décrivent le type d’une variable, d’un paramètre ou d’une valeur de retour :
// primitives
let name: string = "Ada";
let age: number = 36;
let active: boolean = true;
// arrays
let skills: string[] = ["HTML", "CSS"];
let scores: Array<number> = [95, 87, 92];
// function parameters and return
function greet(name: string): string {
return `Hello, ${name}!`;
}
Les annotations ne sont pas toujours nécessaires. TypeScript infère les types à partir de la valeur :
let count = 0; // inferred as number
const items = ["a"]; // inferred as string[]
Privilégiez l’inférence lorsque le type est évident. Utilisez les annotations lorsque le type n’est pas clair d’après la valeur, lorsque vous avez besoin d’un type plus général, ou lorsque l’inférence par défaut est trop restrictive.
Inférence de type
TypeScript déduit automatiquement les types à partir du contexte :
// from assignment
let x = 5; // number
// from return value
function doubling(n: number) {
return n * 2; // inferred as number
}
// from context
const numbers = [1, 2, 3]; // number[]
const doubled = numbers.map(n => n * 2); // number[]
L’inférence vous permet d’écrire moins de code tout en conservant le même niveau de sécurité. La plupart du code TypeScript repose largement sur l’inférence — n’utilisez les annotations que lorsqu’elles apportent de la clarté.
Types d’union (Union types)
Un type d’union indique qu’une valeur peut être l’un parmi plusieurs types :
type Status = "loading" | "success" | "error";
function handleStatus(status: Status) {
if (status === "loading") {
// TypeScript knows status is "loading" here
}
}
let value: string | number;
value = "hello";
value = 42;
Les unions modélisent très bien les données du monde réel. Les réponses d’une API peuvent réussir ou échouer. Un champ peut être une chaîne de caractères ou null. Une fonction peut accepter différents types d’entrées.
Le narrowing de type
TypeScript affine (narrowing) les types d’union à l’intérieur des conditionnels :
function process(value: string | number) {
if (typeof value === "string") {
// TypeScript knows value is string here
return value.toUpperCase();
}
// TypeScript knows value is number here
return value.toFixed(2);
}
Le narrowing fonctionne également avec :
- l’opérateur
in— pour vérifier si une propriété existe instanceof— pour vérifier les instances de classe- les vérifications d’égalité —
===et!== - les unions discriminées — pour vérifier une propriété de marquage (tag) commune
type Circle = { kind: "circle"; radius: number };
type Square = { kind: "square"; side: number };
type Shape = Circle | Square;
function area(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.side ** 2;
}
}
Les unions discriminées sont l’un des patterns les plus puissants de TypeScript. La propriété kind indique précisément à TypeScript avec quelle variante vous travaillez.
Interfaces et alias de type
Une interface décrit la forme d’un objet — ses propriétés, leurs types et si elles sont obligatoires. Un alias de type donne un nom à n’importe quel type, y compris les unions, les tuples, les primitives et les types de fonctions.
// Interface
interface User {
id: number;
name: string;
email: string;
}
// Type alias with the same shape
type User = {
id: number;
name: string;
email: string;
};
Pour les formes d’objets simples, les deux sont interchangeables. Les différences apparaissent lorsque vous composez ou étendez des types.
Propriétés optionnelles et en lecture seule
interface User {
id: number;
name: string;
email?: string; // optional — string | undefined
readonly createdAt: Date;
}
const user: User = { id: 1, name: "Ada", createdAt: new Date() };
user.createdAt = new Date(); // Error: cannot assign to a readonly property
Étendre les interfaces
Les interfaces héritent d’autres interfaces avec extends, y compris plusieurs à la fois :
interface Timestamped {
createdAt: Date;
updatedAt: Date;
}
interface User extends Timestamped {
id: number;
name: string;
}
Alias de type pour les unions, tuples et fonctions
Les alias de type sont particulièrement utiles lorsque le type n’est pas un objet simple :
type ID = string | number;
type Status = "idle" | "loading" | "success" | "error";
type Pair = [string, number];
type Callback = (data: unknown) => void;
Types d’intersection
Combinez des types avec & :
type User = { id: number; name: string };
type WithTimestamp = { createdAt: Date };
type TimestampedUser = User & WithTimestamp;
Interface ou type ?
Utilisez les interfaces pour les formes d’objets susceptibles d’être étendues ou implémentées par des classes. Utilisez les alias de type pour les unions, les intersections, les tuples et les types de fonctions. Choisissez une convention pour les formes d’objets et restez cohérent.
Les Génériques
Les génériques sont des paramètres de type : ils permettent à une fonction, une interface ou une classe de fonctionner avec n’importe quel type tout en préservant la sécurité du typage. Au lieu de coder un type en dur ou de se rabattre sur any, vous passez une variable de type qui est définie lors de l’appel.
function first<T>(items: T[]): T | undefined {
return items[0];
}
const num = first([1, 2, 3]); // number
const str = first(["a", "b"]); // string
Sans les génériques, vous devriez soit utiliser any et perdre les informations de type, soit écrire une fonction distincte pour chaque type.
Fonctions génériques
Un paramètre de type est déclaré entre chevrons. TypeScript l’infère généralement à partir des arguments, vous n’avez donc que rarement besoin de le passer explicitement :
function merge<T, U>(a: T, b: U): T & U {
return { ...a, ...b };
}
const result = merge({ name: "Ada" }, { age: 36 });
// { name: string; age: number }
Interfaces et classes génériques
interface Response<T> {
data: T;
status: number;
}
interface User {
id: number;
name: string;
}
const response: Response<User> = {
data: { id: 1, name: "Ada" },
status: 200,
};
class Stack<T> {
private items: T[] = [];
push(item: T) { this.items.push(item); }
pop(): T | undefined { return this.items.pop(); }
}
Contraintes
Utilisez extends pour restreindre ce qu’un générique peut accepter :
interface HasId {
id: number;
}
function findById<T extends HasId>(items: T[], id: number): T | undefined {
return items.find((item) => item.id === id);
}
Paramètres de type par défaut
interface Paginated<T = unknown> {
data: T[];
total: number;
page: number;
}
const response: Paginated = { data: [1, "a"], total: 10, page: 1 };
Les génériques sont supprimés lors de l’exécution (runtime), ils n’ajoutent donc aucun overhead — ils existent uniquement pour rendre le compilateur et votre éditeur plus intelligents.
Le fichier tsconfig.json
Le fichier tsconfig.json configure le compilateur TypeScript :
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"strict": true,
"outDir": "./dist",
"rootDir": "./src",
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
Options clés :
- target — la version de JavaScript à générer (ES2020 est un bon choix par défaut)
- strict — active tous les drapeaux de vérification de type stricte
- module — le système de modules pour la sortie (ESNext pour les projets modernes)
- outDir — le répertoire de destination du JavaScript compilé
- esModuleInterop — permet les imports par défaut depuis des modules CommonJS
Utilisez toujours "strict": true pour les nouveaux projets. Cela permet de détecter plus de bugs, au prix d’un code légèrement plus explicite.
Installer TypeScript
# As a project dependency (recommended)
npm install --save-dev typescript
# Globally (not recommended for teams)
npm install -g typescript
Compilez votre projet :
# Using npx
npx tsc
# Or add a script to package.json
npm run build
Pour le développement avec le mode watch :
npx tsc --watch
La plupart des frameworks modernes (Next.js, Vite, Astro) gèrent la compilation TypeScript pour vous — vous n’avez pas besoin d’exécuter tsc manuellement.
Erreurs courantes
- Utiliser
anypartout, ce qui désactive la vérification des types. - Annoter chaque élément alors que l’inférence connaît déjà le type.
- Ne pas activer le mode strict dans le tsconfig.
- Confondre les types TypeScript avec les valeurs au runtime — les types sont supprimés lors de la compilation.
- Importer les types de manière incorrecte — utilisez
import typepour les imports de types uniquement. - Ignorer les erreurs du compilateur au lieu de les corriger.
- Ne pas installer les packages
@typespour les bibliothèques JavaScript. - Rendre toutes les propriétés d’une interface optionnelles, ce qui masque les vérifications obligatoires.
- Utiliser
anyau lieu d’un générique, perdant ainsi les informations de type que vous souhaitiez conserver.
Bonnes pratiques
- Activez le mode strict dès le premier jour.
- Laissez l’inférence faire le travail — n’ajoutez des annotations que lorsqu’elles apportent une valeur ajoutée.
- Utilisez
unknownau lieu deanylorsque vous ne connaissez pas le type. - Privilégiez les interfaces pour les formes d’objets susceptibles d’être étendues.
- Utilisez les discriminated unions pour les machines à états et les données variantes.
- Gardez les définitions de types proches de l’endroit où elles sont utilisées.
- Utilisez
import typepour les imports de types uniquement afin de réduire la taille du code au runtime. - Considérez le compilateur comme un coéquipier, et non comme un obstacle.
Quelle est la suite ?
Vous maîtrisez désormais les fondamentaux de TypeScript : annotations, inférence, unions, narrowing, interfaces, alias de type et génériques. La suite logique est de découvrir les Types Avancés pour apprendre les utility types, les types conditionnels et les mapped types. Choisissez un projet, refactorisez du code et laissez la pratique porter ses fruits.