Chapitre 6
Chapitre 06 — TypeScript
Chapitre 06 — TypeScript
Chapitre 06 — TypeScript : Cours Complet
1. Histoire et Motivation
1.1 Le problème JavaScript
JavaScript a été créé en 10 jours par Brendan Eich en 1995 pour Netscape Navigator. Conçu à l'origine pour de petites interactions dans le navigateur, il n'a jamais été pensé pour des applications de la taille de VS Code, Figma ou Teams.
Problèmes fondamentaux de JavaScript pour les grandes codebases :
- Absence de typage statique → les erreurs de type ne sont découvertes qu'à l'exécution
- Documentation implicite : impossible de savoir ce qu'une fonction attend sans lire son implémentation
- Refactoring risqué : renommer une propriété peut casser des centaines de fichiers sans alerte
- Outillage limité : autocomplétion et navigation de code imprécises
1.2 La réponse Microsoft
En 2012, Microsoft dévoile TypeScript, conçu par Anders Hejlsberg. L'objectif n'est pas de remplacer JavaScript mais de l'étendre :
JavaScript + Types = TypeScript
Principes fondateurs :
- TypeScript est un sur-ensemble de JavaScript (tout JS valide est TS valide)
- Le typage est optionnel et progressif
- Les types sont effacés à la compilation (pas de runtime overhead)
- La compilation produit du JavaScript lisible et standard
- L'inférence de types réduit la verbosité
1.3 Adoption industrielle
TypeScript est aujourd'hui adopté par :
- Google (Angular écrit en TS, utilisé en interne)
- Microsoft (VS Code, Teams, Azure SDK)
- Meta (React, Jest, Flow migré vers TS)
- Airbnb, Shopify, Netflix, Asana, et des milliers d'autres
Selon le State of JS Survey 2024, TypeScript est utilisé par plus de 85% des développeurs JavaScript.
2. Types Fondamentaux
2.1 Types primitifs
let name: string = "TypeScript";
let age: number = 30;
let isReady: boolean = true;
let data: null = null;
let value: undefined = undefined;
let big: bigint = 100n;
let sym: symbol = Symbol("unique");
let anything: any = "peut être n'importe quoi";
let unknown: unknown = "doit être vérifié avant usage";
let nothing: void = undefined;
let never: never; // n'arrive jamais (ex: fonction qui throw)
2.2 Type Inference
TypeScript infère les types automatiquement :
let count = 42; // inféré: number
const name = "TS"; // inféré: "TS" (literal type)
let items = [1, 2, 3]; // inféré: number[]
2.3 Unions et Intersections
Union types : une valeur peut être de type A OU B
type Status = "idle" | "loading" | "success" | "error";
type Result<T> = T | Error;
type ID = string | number;
function printID(id: ID) {
if (typeof id === "string") {
console.log(id.toUpperCase());
} else {
console.log(id.toFixed(2));
}
}
Intersection types : une valeur combine les propriétés de A ET B
type Named = { name: string };
type Aged = { age: number };
type Person = Named & Aged;
// { name: string; age: number; }
2.4 Tuples
type Point = [number, number];
type RGB = [red: number, green: number, blue: number];
type HTTPResponse = [status: number, data: unknown];
const origin: Point = [0, 0];
const color: RGB = [255, 128, 0];
2.5 Enums
enum Direction {
Up, // 0
Down, // 1
Left, // 2
Right // 3
}
enum HttpStatus {
OK = 200,
Created = 201,
BadRequest = 400,
Unauthorized = 401,
NotFound = 404,
InternalServerError = 500
}
// Const enum (zéro runtime cost)
const enum Color {
Red = "#FF0000",
Green = "#00FF00",
Blue = "#0000FF"
}
3. Interfaces vs Types
3.1 Interfaces
interface User {
id: string;
name: string;
email: string;
role: "admin" | "user";
readonly createdAt: Date;
updatedAt?: Date;
}
// Declaration merging (uniquement avec interface)
interface User {
lastLogin: Date;
}
// User a maintenant 6 propriétés
3.2 Type Aliases
type User = {
id: string;
name: string;
email: string;
};
// Union types (impossible avec interface)
type Result = User | Error;
// Mapped types
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
3.3 Quand utiliser quoi ?
| Critère | Interface | Type |
|---|---|---|
| Declaration merging | ✅ | ❌ |
| Classes (implements) | ✅ | ✅ |
| Union types | ❌ | ✅ |
| Mapped types | ❌ | ✅ |
| Performance | ✅ (plus rapide) | ⚠️ (plus lent avec des unions larges) |
| Héritage | extends | & (intersection) |
Règle empirique : utilisez interface pour les objets/classes de l'API publique, type pour les unions, utilitaires et transformations.
4. Generics
4.1 Syntaxe de base
function identity<T>(arg: T): T {
return arg;
}
const num = identity(42); // T inféré: number
const str = identity("hello"); // T inféré: string
4.2 Contraintes
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(arg: T): T {
console.log(arg.length);
return arg;
}
logLength("hello"); // OK: string a length
logLength([1, 2, 3]); // OK: Array a length
logLength(42); // Error: number n'a pas length
4.3 Generic Constraints avancées
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "Alice", age: 30, email: "alice@example.com" };
getProperty(user, "name"); // OK: string
getProperty(user, "age"); // OK: number
getProperty(user, "unknown"); // Error
4.4 Conditional Types
type IsString<T> = T extends string ? true : false;
type A = IsString<"hello">; // true
type B = IsString<42>; // false
// Conditional type avec inference (infer)
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type Fn = (x: number) => string;
type R = ReturnType<Fn>; // string
// Distributive conditional types
type ToArray<T> = T extends unknown ? T[] : never;
type Result = ToArray<string | number>; // string[] | number[]
4.5 Infer
type ExtractPromise<T> = T extends Promise<infer U> ? U : T;
type A = ExtractPromise<Promise<string>>; // string
type B = ExtractPromise<number>; // number
type FirstArgument<T> = T extends (first: infer F, ...args: any[]) => any ? F : never;
type Fn = (x: number, y: string) => boolean;
type Arg = FirstArgument<Fn>; // number
5. Utility Types
5.1 Built-in Utility Types
interface Todo {
title: string;
description: string;
completed: boolean;
createdAt: Date;
}
// Partial: toutes les propriétés deviennent optionnelles
type PartialTodo = Partial<Todo>;
// Required: toutes les propriétés deviennent requises
type RequiredTodo = Required<PartialTodo>;
// Pick: sélectionner certaines propriétés
type TodoPreview = Pick<Todo, "title" | "completed">;
// Omit: exclure certaines propriétés
type TodoWithoutDate = Omit<Todo, "createdAt">;
// Record: dictionnaire typé
type PageInfo = Record<string, { title: string; url: string }>;
// Exclude / Extract: unions
type T0 = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
type T1 = Extract<"a" | "b" | "c", "a" | "f">; // "a"
// NonNullable
type T2 = NonNullable<string | number | null | undefined>; // string | number
// Parameters / ReturnType
type Fn = (x: number, y: string) => boolean;
type Params = Parameters<Fn>; // [number, string]
type Ret = ReturnType<Fn>; // boolean
// Awaited
type PromiseResult = Awaited<Promise<Promise<string>>>; // string
5.2 Mapped Types
// Rendre toutes les propriétés readonly
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};
// Rendre toutes les propriétés optionnelles
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
// Modifier le type des propriétés
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
// Avec remapping de clés (TS 4.1+)
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type Person = { name: string; age: number };
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number; }
5.3 Template Literal Types
type EventName = "click" | "focus" | "blur";
type HandlerName = `on${Capitalize<EventName>}`;
// "onClick" | "onFocus" | "onBlur"
type CSSValue = number | string;
type CSSProperty = "margin" | "padding" | "border";
type CSSDeclaration = `${CSSProperty}-${"top" | "right" | "bottom" | "left"}`;
// "margin-top" | "margin-right" | ... | "border-left"
// Parsing avec infer
type ParseRoute<T extends string> =
T extends `${infer Base}/:${infer Param}`
? { base: Base; param: Param }
: never;
type Route = ParseRoute<"/users/:id">;
// { base: "/users"; param: "id" }
6. Type Guards
6.1 typeof
function format(value: string | number) {
if (typeof value === "string") {
return value.toUpperCase(); // TS sait que c'est string
}
return value.toFixed(2); // TS sait que c'est number
}
6.2 instanceof
class ApiError extends Error {
constructor(public statusCode: number, message: string) {
super(message);
}
}
function handleError(error: Error | ApiError) {
if (error instanceof ApiError) {
console.log(`API Error ${error.statusCode}: ${error.message}`);
} else {
console.log(`Generic Error: ${error.message}`);
}
}
6.3 Custom Type Predicates
interface Fish { swim(): void; }
interface Bird { fly(): void; }
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
function move(pet: Fish | Bird) {
if (isFish(pet)) {
pet.swim(); // OK: type narrowé vers Fish
} else {
pet.fly(); // OK: type narrowé vers Bird
}
}
6.4 Discriminated Unions
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rectangle"; width: number; height: number }
| { kind: "triangle"; base: number; height: number };
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "rectangle":
return shape.width * shape.height;
case "triangle":
return (shape.base * shape.height) / 2;
default:
const _exhaustive: never = shape;
return _exhaustive;
}
}
7. Declaration Files (.d.ts)
7.1 Structure d'un .d.ts
// math.d.ts
export function add(a: number, b: number): number;
export function subtract(a: number, b: number): number;
export const PI: 3.14159;
export interface Calculator {
add(a: number, b: number): number;
memory: number;
}
7.2 Ambient Declarations
// Pour déclarer un module sans typages
declare module "*.css" {
const classes: { [key: string]: string };
export default classes;
}
declare module "*.svg" {
const content: string;
export default content;
}
// Déclarer une variable globale
declare const __VERSION__: string;
declare function trackEvent(name: string, data?: Record<string, unknown>): void;
7.3 Augmentation de module
// express.d.ts
import "express";
declare module "express" {
interface Request {
user?: {
id: string;
role: "admin" | "user";
};
}
}
8. Configuration tsconfig.json
8.1 Configuration strict minimale
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/components/*"],
"@utils/*": ["src/utils/*"]
}
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}
8.2 Strict Mode expliqué
// strict: true active ces 7 flags :
// - strictNullChecks: null/undefined ne sont pas assignables à tout type
// - noImplicitAny: interdit les `any` implicites
// - strictFunctionTypes: variance checking sur les fonctions
// - strictBindCallApply: bind/call/apply correctement typés
// - strictPropertyInitialization: propriétés doivent être initialisées
// - noImplicitThis: interdit le this implicite
// - alwaysStrict: strict mode JS dans chaque fichier
8.3 Module Resolution
// Classic (legacy) vs Node (pour Node.js) vs Bundler (pour Vite/Webpack)
// NodeNext: respecte la résolution ESM de Node.js
// moduleResolution: "bundler" — le plus flexible pour projets modernes
// Permet d'omettre les extensions .js dans les imports
9. TypeScript avec React
9.1 Typage des Props
interface ButtonProps {
label: string;
variant?: "primary" | "secondary" | "danger";
size?: "sm" | "md" | "lg";
disabled?: boolean;
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
children?: React.ReactNode;
}
const Button: React.FC<ButtonProps> = ({
label,
variant = "primary",
size = "md",
disabled = false,
onClick,
children
}) => {
return (
<button
className={`btn btn-${variant} btn-${size}`}
disabled={disabled}
onClick={onClick}
>
{label}
{children}
</button>
);
};
9.2 Typage des Hooks
// useState avec inférence
const [count, setCount] = useState(0); // type: number
const [user, setUser] = useState<User | null>(null);
// useReducer typé
type Action =
| { type: "INCREMENT"; payload: number }
| { type: "DECREMENT" }
| { type: "RESET" };
interface State {
count: number;
lastAction: string;
}
const initialState: State = { count: 0, lastAction: "INIT" };
function reducer(state: State, action: Action): State {
switch (action.type) {
case "INCREMENT":
return { count: state.count + action.payload, lastAction: "INCREMENT" };
case "DECREMENT":
return { count: state.count - 1, lastAction: "DECREMENT" };
case "RESET":
return initialState;
default:
return state;
}
}
// useRef
const inputRef = useRef<HTMLInputElement>(null);
const intervalRef = useRef<number | null>(null);
// useMemo / useCallback avec typage implicite
const sortedItems = useMemo(() => {
return items.sort((a, b) => a.name.localeCompare(b.name));
}, [items]);
const handleClick = useCallback((e: React.MouseEvent) => {
console.log(e.clientX, e.clientY);
}, []);
9.3 Typage des Events
// onChange pour input
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value);
};
// onSubmit pour formulaire
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
// traitement
};
// onDrag
const handleDrag = (e: React.DragEvent<HTMLDivElement>) => {
e.dataTransfer.setData("text/plain", "data");
};
10. Bonnes Pratiques et Patterns Avancés
10.1 Avoid any
// ❌ À éviter
function process(data: any) {
return data.value;
}
// ✅ Préférer unknown
function process(data: unknown) {
if (data && typeof data === "object" && "value" in data) {
return (data as { value: unknown }).value;
}
throw new Error("Invalid data");
}
10.2 Branded Types
type Brand<T, B> = T & { __brand: B };
type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;
function getUser(id: UserId) { /* ... */ }
function getOrder(id: OrderId) { /* ... */ }
const userId = "abc" as UserId;
const orderId = "xyz" as OrderId;
getUser(userId); // OK
getUser(orderId); // Error: Type '"OrderId"' is not assignable
10.3 Builder Pattern avec Types
class QueryBuilder<T extends Record<string, unknown>> {
private conditions: Partial<T> = {};
where<K extends keyof T>(key: K, value: T[K]): this {
this.conditions[key] = value;
return this;
}
build(): Partial<T> {
return { ...this.conditions };
}
}
interface UserQuery {
name: string;
age: number;
email: string;
}
const query = new QueryBuilder<UserQuery>()
.where("name", "Alice")
.where("age", 30)
.build();
10.4 Flatten / DeepPartial
type DeepPartial<T> = T extends object
? { [P in keyof T]?: DeepPartial<T[P]> }
: T;
type DeepReadonly<T> = T extends object
? { readonly [P in keyof T]: DeepReadonly<T[P]> }
: T;
interface Config {
server: {
host: string;
port: number;
ssl: {
enabled: boolean;
cert: string;
};
};
}
type PartialConfig = DeepPartial<Config>;
// tout devient optionnel récursivement
10.5 Type-safe Event Emitter
type EventMap = {
userCreated: { id: string; name: string };
userDeleted: { id: string };
error: { message: string; code: number };
};
class TypedEmitter<T extends Record<string, unknown>> {
private listeners: Map<keyof T, Set<(...args: any[]) => void>> = new Map();
on<K extends keyof T>(event: K, callback: (data: T[K]) => void): void {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event)!.add(callback);
}
emit<K extends keyof T>(event: K, data: T[K]): void {
this.listeners.get(event)?.forEach(cb => cb(data));
}
}
const emitter = new TypedEmitter<EventMap>();
emitter.on("userCreated", (data) => {
console.log(data.name); // typé!
});
11. Comparaison : TypeScript vs Flow vs JSDoc
| Critère | TypeScript | Flow (Meta) | JSDoc |
|---|---|---|---|
| Performance compilation | ⚡ Rapide (tsserver) | ⚠️ Lent | ✅ Aucune |
| Écosystème | 🌟 Excellent | ⚠️ Limité | ✅ Universel |
| Adoption | 85%+ des devs | < 5% | ~30% (VS Code) |
| Strictness | Configurable | Strict par défaut | Limité |
| IDE Support | Excellent (LSP) | Correct | Bon (VS Code) |
| Fonctionnalités | Completes | Partielles | Basiques |
| Migration | Compilateur | Checker uniquement | Aucune |
12. Erreurs Courantes des Juniors
// ❌ any partout
function fetchData(): any { /* ... */ }
// ✅ Typer correctement
function fetchData(): Promise<ApiResponse> { /* ... */ }
// ❌ Surcharger inutilement
const items: string[] = ["a", "b"];
// ✅ Laisser l'inférence faire son travail
const items = ["a", "b"];
// ❌ as partout
const user = data as User;
// ✅ Type narrowing
if (isUser(data)) { /* ... */ }
// ❌ Ignorer strictNullChecks
function getName(user: User | null) {
return user.name; // 💥 Runtime error
}
// ✅ Vérifier null
function getName(user: User | null) {
return user?.name ?? "Anonymous";
}