Skip to content

TypeScript Aide-mémoire

Sur-ensemble typé de JavaScript qui passe à l'échelle.

01

Premiers pas

Hello World & Compilation

Les fichiers TypeScript utilisent l'extension .ts. Le compilateur tsc transpile TS en JS, effaçant toutes les annotations de type à l'exécution. Utilisez --strict pour une sécurité de type maximale. ts-node ou bun peuvent exécuter les fichiers .ts directement sans étape de compilation séparée.

typescript
// hello.ts
const message: string = "Hello, TypeScript!";
console.log(message);

// Compile to JavaScript (type annotations erased):
//   tsc hello.ts          -> hello.js
//   tsc --strict hello.ts  // enable all strict checks
//   tsc --watch hello.ts   // recompile on change

// Run directly with ts-node or bun:
//   ts-node hello.ts

tsconfig.json

tsconfig.json configure le compilateur TypeScript. 'strict: true' active noImplicitAny, strictNullChecks, strictFunctionTypes et plus. 'target' contrôle la version JS de sortie. 'esModuleInterop' active les imports par défaut depuis les modules CommonJS comme les intégrés de Node.

typescript
{
  "compilerOptions": {
    "target": "ES2020",        // JS version to emit
    "module": "ESNext",        // module system
    "strict": true,            // enable all strict checks
    "outDir": "./dist",        // output directory
    "rootDir": "./src",        // source root
    "esModuleInterop": true,   // allow default imports from CJS
    "skipLibCheck": true,      // skip .d.ts type checking
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

Annotations de type & Inférence

Les annotations de type spécifient explicitement le type d'une variable. TypeScript peut aussi inférer les types depuis les valeurs. Utilisez des annotations explicites pour les signatures de fonction et les APIs publiques ; reposez-vous sur l'inférence pour les variables locales. Évitez 'any' — il désactive entièrement la vérification de type.

typescript
// Explicit type annotations
let count: number = 0;
const name: string = "Alice";
let isDone: boolean = true;
let ids: number[] = [1, 2, 3];
let tuple: [string, number] = ["age", 30];

// Type inference (let TS figure it out)
let age = 30;           // inferred as number
let items = [1, 2, 3];  // inferred as number[]
let mixed = [1, "a"];   // inferred as (string | number)[]

// any disables type checking (avoid!)
let anything: any = 42;
anything = "string";    // no error

Vérifications du mode strict

Le mode strict active des vérifications critiques : strictNullChecks (null/undefined non assignables à d'autres types), noImplicitAny (les paramètres doivent avoir des types), strictPropertyInitialization (les champs de classe doivent être initialisés). Utilisez '!' (affectation définie) lorsque vous êtes sûr qu'un champ sera défini plus tard.

typescript
// strictNullChecks: null/undefined not assignable to other types
let n: string = null; // Error in strict mode

// noImplicitAny: must annotate parameters
function greet(name) { } // Error: implicit any

// strictPropertyInitialization
class User {
  name: string; // Error: not initialized
  constructor() {}
}

// Fix: initialize or use definite assignment
class Fixed {
  name!: string; // definite assignment assertion
  age: number = 0;
}

Fichiers de déclaration (.d.ts)

Les fichiers de déclaration (.d.ts) fournissent des types pour les bibliothèques JavaScript sans définitions TypeScript. 'declare' indique au compilateur qu'une variable/fonction existe à l'exécution. Utilisez les packages @types de DefinitelyTyped pour les bibliothèques populaires (ex. @types/node, @types/react).

typescript
// types.d.ts - type declarations only, no implementation
declare module "my-lib" {
  export function greet(name: string): string;
  export const version: string;
}

// global.d.ts - extend global scope
declare global {
  interface Window {
    myApp: { version: string };
  }
}

// Usage in .ts files:
// import { greet } from "my-lib";  // now typed
// Install @types packages: npm i -D @types/node @types/react
02

Types de base

Primitifs & Types spéciaux

Primitifs TypeScript : string, number, boolean, bigint, symbol. 'void' indique qu'une fonction ne renvoie pas de valeur. 'never' représente des valeurs qui ne se produisent jamais — fonctions qui lèvent des exceptions ou tournent indéfiniment. Utilisez 'never' pour les vérifications exhaustives dans les instructions switch.

typescript
let str: string = "hello";
let num: number = 42;
let bool: boolean = true;
let big: bigint = 100n;
let sym: symbol = Symbol("id");

// void: function returns nothing
function log(msg: string): void { console.log(msg); }

// never: function never returns
function fail(msg: string): never { throw new Error(msg); }
function infinite(): never { while (true) {} }

Tableaux & Tuples

Les tableaux utilisent soit la syntaxe T[] soit Array<T>. ReadonlyArray empêche les mutations. Les tuples sont des tableaux à longueur fixe avec des types spécifiques à chaque index — utiles pour les paires clé-valeur ou les données de type CSV. Les tuples étiquetés améliorent la lisibilité avec des positions nommées.

typescript
// Two syntaxes for arrays
let nums: number[] = [1, 2, 3];
let strs: Array<string> = ["a", "b"];

// ReadonlyArray (immutable)
const ro: ReadonlyArray<number> = [1, 2, 3];
// ro.push(4); // Error: not mutable

// Tuple (fixed length, known types)
let tuple: [string, number] = ["Alice", 30];
let name = tuple[0]; // string
let age = tuple[1];  // number

// Labeled tuple elements (TS 4.0+)
let entry: [name: string, age: number] = ["Bob", 25];

Enums

Les enums définissent un ensemble de constantes nommées. Les enums de chaîne sont recommandés pour le débogage (les valeurs sont lisibles en sortie). Les enums numériques prennent en charge le mappage inverse. 'const enum' est effacé à la compilation (intégré) pour un coût d'exécution nul. Préférez les types union pour les cas simples.

typescript
// Numeric enum (default starts at 0)
enum Direction { Up, Down, Left, Right }
let d: Direction = Direction.Up;  // 0

// String enum (recommended for readability)
enum Status {
  Pending = "PENDING",
  Success = "SUCCESS",
  Failed = "FAILED",
}

// Reverse mapping (numeric only)
console.log(Direction[0]); // "Up"

// Const enum (inlined, no runtime object)
const enum Color { Red, Green, Blue }
let c = Color.Red; // compiles to: let c = 0;

any vs unknown vs never

'any' désactive toute vérification de type — évitez-le. 'unknown' est l'alternative de type sûr : vous devez le restreindre (via typeof, instanceof) avant usage. 'never' représente des valeurs qui ne se produisent jamais, utilisé pour la vérification d'exhaustivité dans les instructions switch pour capturer les cas manquants à la compilation.

typescript
// any: opt out of type checking (dangerous!)
let a: any = 42;
a = "string";       // OK
a.toUpperCase();    // OK (no check, may fail at runtime)

// unknown: type-safe alternative to any
let u: unknown = 42;
// u.toUpperCase(); // Error: unknown type
if (typeof u === "string") {
  u.toUpperCase();  // OK after narrowing
}

// never: impossible value (exhaustiveness check)
type Shape = "circle" | "square";
function area(s: Shape) {
  switch (s) {
    case "circle": return Math.PI;
    case "square": return 1;
    default:
      const _exhaustive: never = s; // Error if case missing
  }
}

Assertions de type

Les assertions de type disent au compilateur 'faites-moi confiance, je connais le type'. Utilisez la syntaxe 'as'. L'assertion non-nulle (!) dit à TS qu'une valeur n'est pas null/undefined. 'as const' rend toutes les propriétés des littéraux en lecture seule — utile pour les objets de config et les types d'action Redux. Les assertions ne changent pas le comportement à l'exécution.

typescript
// as syntax (preferred, works in .tsx)
let val: unknown = "hello";
let len: number = (val as string).length;

// Non-null assertion (!)
let el = document.querySelector("#app")!;
el.innerHTML = "Hi";  // el is HTMLElement, not null

// Double assertion (for unsafe casts)
let value = "42" as unknown as number;

// const assertion (literal types)
const req = { method: "GET", url: "/api" } as const;
// req.method has type "GET" (not string)
// req is readonly

Types littéraux & Union

Les types littéraux restreignent une valeur à une chaîne, un nombre ou un booléen spécifique. Combinés aux unions, ils créent des types précis comme direction ou méthodes HTTP. Les types de littéraux de gabarit (TS 4.1+) construisent des types de chaîne depuis d'autres types — puissants pour générer des clés de type sûr et des noms d'événements.

typescript
// String literal types
let direction: "left" | "right" | "up" | "down";
direction = "left";  // OK
// direction = "sideways"; // Error

// Numeric literal types
let dice: 1 | 2 | 3 | 4 | 5 | 6;
dice = 4;  // OK

// Boolean literal
let flag: true = true;

// Template literal types (TS 4.1+)
type Color = "red" | "blue";
type Size = "small" | "large";
type Variant = `${Size}-${Color}`;
// "small-red" | "small-blue" | "large-red" | "large-blue"
03

Interfaces & Objets

Bases des interfaces

Les interfaces décrivent la forme des objets. '?' marque les propriétés optionnelles (peuvent être undefined). 'readonly' empêche la réaffectation après initialisation. Les interfaces sont à la compilation uniquement — elles sont effacées dans le JavaScript de sortie. Utilisez-les pour définir des contrats pour les objets et les classes.

typescript
interface User {
  id: number;
  name: string;
  email?: string;        // optional property
  readonly createdAt: Date; // immutable
}

const user: User = {
  id: 1,
  name: "Alice",
  createdAt: new Date(),
};

// user.createdAt = new Date(); // Error: readonly
// user.email; // string | undefined

Signatures d'index

Les signatures d'index permettent des objets avec des clés arbitraires d'un type donné. Toutes les valeurs de propriété doivent être assignables au type d'index. Utile pour les dictionnaires, caches et données dynamiques. Combinez avec des propriétés connues pour des configs typées avec des options supplémentaires.

typescript
// Index signature: arbitrary string keys
interface StringMap {
  [key: string]: string;
}

const dict: StringMap = {
  name: "Alice",
  city: "NYC",
  // count: 42, // Error: value must be string
};

// Mixed: known + index signature
interface Config {
  name: string;
  [key: string]: string | number;
}

// Readonly index signature
interface ReadonlyMap {
  readonly [key: string]: number;
}

Étendre des interfaces

Les interfaces peuvent étendre une ou plusieurs autres interfaces, combinant leurs membres. Cela permet la composition et la réutilisation de code. Contrairement aux classes, les interfaces prennent en charge l'héritage multiple. Lorsque vous implémentez une interface, la classe doit fournir tous les membres requis.

typescript
interface Animal {
  name: string;
  eat(): void;
}

interface Pet extends Animal {
  owner: string;
  play(): void;
}

interface Swimmer {
  swim(): void;
}

// Multiple inheritance
interface Duck extends Pet, Swimmer {
  quack(): void;
}

const duck: Duck = {
  name: "Donald",
  owner: "Walt",
  eat() {},
  play() {},
  swim() {},
  quack() {},
};

Types de fonction dans les interfaces

Les interfaces peuvent décrire des signatures de fonction, permettant des rappels de type sûr. Les interfaces hybrides (appelables + propriétés) sont utilisées pour les fonctions de style jQuery qui ont aussi des méthodes. Ce motif est courant dans les bibliothèques qui renvoient des fonctions avec des assistants attachés.

typescript
interface SearchFn {
  (source: string, keyword: string): boolean;
}

const contains: SearchFn = (src, kw) => src.includes(kw);
console.log(contains("hello world", "world")); // true

// Interface with mixed members (hybrid)
interface Counter {
  (start: number): void;  // callable
  count: number;           // property
  reset(): void;           // method
}

// Function with properties (jQuery-style)
const counter: any = (n: number) => { counter.count = n; };
counter.count = 0;
counter.reset = () => { counter.count = 0; };

Interface vs Alias de type

Les interfaces prennent en charge la fusion de déclaration (les interfaces de même nom se combinent), de meilleurs messages d'erreur, et sont préférées pour les formes d'objet/classe. Les alias de type sont plus flexibles (peuvent représenter des unions, primitifs, tuples) mais ne peuvent pas être fusionnés. Utilisez les interfaces pour les APIs extensibles, les alias de type pour les unions et les types calculés.

typescript
// Interface: extendable, better error messages
interface Window { title: string; }
interface Window { size: number; } // declaration merging
const w: Window = { title: "App", size: 800 };

// Type alias: more flexible (unions, primitives, etc.)
type ID = string | number;
type Callback<T> = (value: T) => void;

// Both can describe object shapes
interface UserI { name: string; }
type UserT = { name: string; };

// Use interface for objects/classes, type for unions/aliases

Chaînage optionnel & Coalescence des nuls

Le chaînage optionnel (?.) accède sûrement aux propriétés imbriquées — renvoie undefined au lieu de lever si un maillon est null/undefined. La coalescence des nuls (??) fournit une valeur par défaut uniquement pour null/undefined (pas 0 ou ''). Ces opérateurs réduisent considérablement le code verbeux de vérification de null.

typescript
interface User {
  profile?: {
    address?: {
      city?: string;
    };
  };
}

const user: User = {};

// Optional chaining (?.) - safe property access
const city = user.profile?.address?.city; // string | undefined

// Nullish coalescing (??) - default value
const name = user.profile?.address?.city ?? "Unknown";

// Non-null assertion (!) - you're sure it's not null
// const c = user.profile!.address!.city!; // risky

// Optional method call
const result = user.profile?.address?.city?.toUpperCase();
04

Alias de type & Unions

Alias de type

Les alias de type créent des références nommées à tout type, y compris les unions, intersections, primitifs et génériques. Contrairement aux interfaces, les alias ne peuvent pas être fusionnés ou étendus, mais ils sont plus flexibles. Utilisez les alias pour les unions, tuples et types utilitaires ; utilisez les interfaces pour les formes d'objet.

typescript
// Basic alias
type ID = string | number;
type Point = { x: number; y: number };

// Generic alias
type Container<T> = { value: T };

// Function type alias
type Handler<T> = (event: T) => void;

// Usage
const id: ID = 42;
const p: Point = { x: 1, y: 2 };
const box: Container<string> = { value: "hi" };
const onClick: Handler<string> = (e) => console.log(e);

Types union

Les types union (A | B) permettent à une valeur d'être l'un de plusieurs types. TypeScript restreint le type dans les blocs conditionnels en utilisant typeof, instanceof ou in. Note : (string | number)[] est différent de string[] | number[] — le premier est un tableau mixte, le second est tout-chaînes OU tout-nombres.

typescript
// Union: value can be one of several types
type ID = string | number;

function display(id: ID) {
  if (typeof id === "string") {
    console.log(id.toUpperCase()); // narrowed to string
  } else {
    console.log(id.toFixed(2));    // narrowed to number
  }
}

display("abc");  // ABC
display(42);     // 42.00

// Union of arrays vs array of unions
type Mixed = (string | number)[];
type Either = string[] | number[];

Types intersection

Les types intersection (A & B) combinent tous les membres de plusieurs types — le résultat doit satisfaire chaque type. Utile pour les mixins, la composition et la fusion des types utilitaires. Contrairement à l'union (OU), l'intersection est ET : la valeur doit avoir toutes les propriétés de tous les types.

typescript
// Intersection: combine multiple types into one
interface BusinessPartner {
  name: string;
  credit: number;
}

interface Identity {
  id: number;
  email: string;
}

type Employee = BusinessPartner & Identity;

const emp: Employee = {
  name: "Alice",
  credit: 1000,
  id: 1,
  email: "[email protected]",
};

// All properties required
// const bad: Employee = { name: "Bob" }; // Error: missing props

Types nullables

En mode strict, null et undefined ne sont pas assignables à d'autres types — vous devez les inclure explicitement avec des unions (string | null). Les paramètres optionnels (param?) sont implicitement T | undefined. Utilisez ?? pour des valeurs par défaut sûres et ! pour affirmer non-null (utilisez avec parcimonie).

typescript
// In strict mode, null/undefined are separate types
let name: string | null = null;
name = "Alice";  // OK

// Optional parameters are implicitly | undefined
function greet(name?: string) {
  // name is string | undefined
  console.log(name ?? "Guest");
}

// Return type can be null
function find(id: number): string | null {
  return id === 1 ? "Alice" : null;
}

// Non-null assertion
const result = find(1)!.toUpperCase(); // "ALICE"

Opérateurs Keyof & Typeof

'keyof T' extrait les clés du type T comme une union de littéraux de chaîne. 'typeof x' extrait le type d'une valeur (utile pour inférer depuis des objets). 'keyof typeof obj' combine les deux pour obtenir les clés d'un objet existant — courant dans les types d'action Redux et les accesseurs de propriété de type sûr.

typescript
interface User {
  id: number;
  name: string;
  email: string;
}

// keyof: extract keys as a union
type UserKey = keyof User; // "id" | "name" | "email"

function getProp(obj: User, key: keyof User) {
  return obj[key];
}

// typeof: extract type from a value
const config = { port: 3000, host: "localhost" };
type Config = typeof config; // { port: number; host: string }

// keyof typeof: keys of an object
type ConfigKey = keyof typeof config; // "port" | "host"

Types mappés

Les types mappés itèrent sur les clés pour transformer un type. Les utilitaires intégrés comme Readonly, Partial et Pick sont des types mappés. Utilisez les modificateurs + et - pour ajouter/supprimer readonly ou optionnel. Le remappage de clés (TS 4.1+) renomme les clés en utilisant des types de littéraux de gabarit — puissant pour générer des types getter/setter.

typescript
// Map over keys to create a new type
type Readonly<T> = {
  readonly [K in keyof T]: T[K];
};

type Optional<T> = {
  [K in keyof T]?: T[K];
};

interface User { id: number; name: string; }

type ReadonlyUser = Readonly<User>;   // all readonly
type OptionalUser = Optional<User>;   // all optional

// Modifiers: +add, -remove
type Mutable<T> = { -readonly [K in keyof T]: T[K]; };
type Required<T> = { [K in keyof T]-?: T[K]; };

// Key remapping (TS 4.1+)
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
05

Fonctions

Types de fonction & Signatures

TypeScript ajoute des annotations de type aux paramètres de fonction et aux valeurs de retour. Le type de retour peut souvent être inféré, mais l'annotation explicite est recommandée pour les APIs publiques. Les paramètres par défaut rendent les arguments optionnels avec une valeur de secours. Utilisez void lorsqu'une fonction ne renvoie rien.

typescript
// Named function with types
function add(a: number, b: number): number {
  return a + b;
}

// Arrow function with types
const multiply = (a: number, b: number): number => a * b;

// Function type alias
type MathOp = (a: number, b: number) => number;
const divide: MathOp = (a, b) => a / b;

// Void return (no return value)
function log(msg: string): void { console.log(msg); }

// Optional and default parameters
function greet(name: string, greeting: string = "Hi"): string {
  return `${greeting}, ${name}!`;
}
greet("Alice");        // "Hi, Alice!"
greet("Bob", "Hello"); // "Hello, Bob!"

Paramètres rest & Tuples

Les paramètres rest (...args) collectent plusieurs arguments dans un tableau. TypeScript les type comme T[] ou un tuple pour les fonctions variadiques à longueur fixe. L'opérateur spread (...) fait l'inverse — déplie un tableau en arguments individuels. Les types rest de tuple permettent des signatures variadiques précises.

typescript
// Rest parameters (variadic)
function sum(...nums: number[]): number {
  return nums.reduce((a, b) => a + b, 0);
}
console.log(sum(1, 2, 3, 4)); // 10

// Tuple rest (fixed prefix + variadic tail)
function pair(name: string, ...scores: number[]): void {
  console.log(name, scores);
}

// Spread call
const nums = [1, 2, 3];
console.log(sum(...nums));

// Typed rest as tuple
function f(...args: [string, number, boolean]): void {
  const [s, n, b] = args;
}

Surcharges de fonction

Les surcharges de fonction fournissent plusieurs signatures de type pour la même fonction, permettant des types de retour précis basés sur l'entrée. La signature d'implémentation est cachée aux appelants. Les surcharges sont résolues de haut en bas — mettez les signatures les plus spécifiques en premier. Courant dans les bibliothèques comme jQuery et lodash.

typescript
// Overload signatures (what callers see)
function parse(input: string): string[];
function parse(input: number): number[];
// Implementation signature (not visible to callers)
function parse(input: string | number): string[] | number[] {
  if (typeof input === "string") {
    return input.split(",");
  }
  return [input, input * 2];
}

const strs = parse("a,b,c"); // string[]
const nums = parse(42);      // number[]

// Overloads with different param counts
function makeDate(timestamp: number): Date;
function makeDate(y: number, m: number, d: number): Date;
function makeDate(yOrTs: number, m?: number, d?: number): Date {
  return m === undefined
    ? new Date(yOrTs)
    : new Date(yOrTs, m - 1, d);
}

Type this

TypeScript vous permet de déclarer le type 'this' comme premier paramètre. Cela garantit que la fonction est appelée avec le bon contexte — utile pour les méthodes passées comme rappels. Les fonctions fléchées capturent 'this' lexicalement, évitant le besoin de .bind(). Utilisez 'noImplicitThis' pour capturer les erreurs de 'this' non typé.

typescript
interface Card {
  suit: string;
  rank: string;
  isFaceUp(): boolean;
}

// Explicit 'this' parameter
function format(this: Card): string {
  return `${this.rank} of ${this.suit}`;
}

const card: Card = {
  suit: "Hearts",
  rank: "A",
  isFaceUp() { return true; },
  format,
};

// 'this' in callbacks with bind
class Handler {
  private count = 0;
  increment = () => { this.count++; }; // arrow binds this
}

Rappels & Fonctions d'ordre supérieur

TypeScript type entièrement les fonctions d'ordre supérieur (fonctions qui prennent ou renvoient des fonctions). Utilisez des paramètres de type générique (T, U) pour préserver les relations de type entre l'entrée et la sortie. Les types de rappel sont couramment définis comme des alias de type pour la réutilisation. Le curry (renvoyer des fonctions) est entièrement de type sûr.

typescript
// Function as parameter
type Callback<T> = (value: T, index: number) => void;

function forEach<T>(arr: T[], cb: Callback<T>): void {
  for (let i = 0; i < arr.length; i++) {
    cb(arr[i], i);
  }
}

forEach(["a", "b"], (v, i) => console.log(i, v));

// Function returning function (curry)
function add(a: number): (b: number) => number {
  return (b) => a + b;
}
const add5 = add(5);
console.log(add5(3)); // 8

// Generic map
function map<T, U>(arr: T[], fn: (x: T) => U): U[] {
  return arr.map(fn);
}

Déstructuration de paramètres

TypeScript prend en charge la déstructuration dans les paramètres de fonction — annotez la forme déstructurée en ligne ou via une interface. L'extraction vers une interface améliore la lisibilité et la réutilisation. La déstructuration de tableau/tuple fonctionne aussi. Ce motif est courant dans les props de composant React et les gestionnaires d'API.

typescript
// Destructured parameters with types
function createUser({ name, age, email }: {
  name: string;
  age: number;
  email?: string;
}): void {
  console.log(name, age, email);
}

createUser({ name: "Alice", age: 30 });

// Extract to interface for reuse
interface UserOpts {
  name: string;
  age: number;
  email?: string;
}

function updateUser({ name, age }: UserOpts): void {}

// Array destructuring in params
function swap([a, b]: [number, number]): [number, number] {
  return [b, a];
}
06

Classes & POO

Classe & Constructeur

Les classes TypeScript prennent en charge les propriétés de paramètre — préfixer les params du constructeur avec des modificateurs d'accès (public/private/protected/readonly) crée et assigne automatiquement les champs. Ce raccourci réduit le passe-partout. Les méthodes peuvent avoir des annotations de type sur les valeurs de retour. Les champs sont par défaut public.

typescript
class Person {
  // Parameter properties (shorthand)
  constructor(
    public name: string,    // auto-creates this.name
    private age: number,    // private field
    readonly id: number,    // immutable
  ) {}

  greet(): string {
    return `Hi, I'm ${this.name}`;
  }
}

const p = new Person("Alice", 30, 1);
console.log(p.name);  // "Alice" (public)
// p.age; // Error: private
// p.id = 2; // Error: readonly

Modificateurs d'accès

Modificateurs d'accès : public (par défaut, partout), private (classe uniquement), protected (classe + sous-classes), readonly (immuable). Le 'private' de TypeScript est à la compilation uniquement ; les champs privés '#' ES sont véritablement privés à l'exécution. Utilisez private pour les détails d'implémentation, protected pour les points d'extension.

typescript
class BankAccount {
  public owner: string;       // accessible everywhere
  private balance: number;    // class only
  protected rate: number;     // class + subclasses
  readonly id: string;        // immutable after init
  #secret: string;            // ES private (runtime)

  constructor(owner: string) {
    this.owner = owner;
    this.balance = 0;
    this.rate = 0.05;
    this.id = crypto.randomUUID();
    this.#secret = "hidden";
  }

  deposit(amount: number): void {
    this.balance += amount;
  }
}

Héritage & Classes abstraites

Les classes abstraites ne peuvent pas être instanciées directement — elles définissent une base pour les sous-classes. Les méthodes abstraites n'ont pas d'implémentation dans la classe de base ; les sous-classes doivent les implémenter. Utilisez 'extends' pour l'héritage et 'super()' pour appeler le constructeur parent. Les classes abstraites permettent le polymorphisme — le code peut fonctionner avec toute sous-classe de Shape.

typescript
abstract class Shape {
  constructor(public color: string) {}
  abstract area(): number;  // must be implemented
  describe(): string {
    return `${this.color} shape, area ${this.area()}`;
  }
}

class Circle extends Shape {
  constructor(color: string, private r: number) {
    super(color);
  }
  area(): number { return Math.PI * this.r ** 2; }
}

class Square extends Shape {
  constructor(color: string, private side: number) {
    super(color);
  }
  area(): number { return this.side ** 2; }
}

const c = new Circle("red", 5);
console.log(c.describe()); // "red shape, area 78.54..."
// new Shape("blue"); // Error: cannot instantiate abstract

Interfaces & Implements

Une classe peut implémenter plusieurs interfaces (séparées par des virgules). La classe doit fournir tous les membres de l'interface. Contrairement à extends (héritage simple), implements prend en charge plusieurs contrats. C'est la méthode de TypeScript pour réaliser un comportement de type héritage multiple. Utilisez les interfaces pour définir des contrats, les classes pour les implémenter.

typescript
interface Printable {
  toString(): string;
}

interface Comparable<T> {
  compareTo(other: T): number;
}

class Money implements Printable, Comparable<Money> {
  constructor(private amount: number) {}

  toString(): string {
    return `$${this.amount.toFixed(2)}`;
  }

  compareTo(other: Money): number {
    return this.amount - other.amount;
  }
}

const a = new Money(10);
const b = new Money(20);
console.log(a.toString());      // "$10.00"
console.log(a.compareTo(b));    // -10

Getters & Setters

Les getters et setters interceptent l'accès aux propriétés pour la validation, le calcul ou les effets secondaires. Utilisez un champ de stockage privé (convention : préfixe tiret bas). Les getters permettent les propriétés calculées (comme fahrenheit depuis celsius). Les setters permettent la validation. Accédez-y comme des propriétés régulières — pas de parenthèses.

typescript
class Temperature {
  private _celsius: number = 0;

  get celsius(): number {
    return this._celsius;
  }

  set celsius(value: number) {
    if (value < -273.15) throw new Error("Below absolute zero");
    this._celsius = value;
  }

  get fahrenheit(): number {
    return this._celsius * 9 / 5 + 32;
  }

  set fahrenheit(value: number) {
    this.celsius = (value - 32) * 5 / 9;
  }
}

const temp = new Temperature();
temp.celsius = 25;
console.log(temp.fahrenheit); // 77
// temp.celsius = -300; // throws

Membres statiques & Singletons

Les membres statiques appartiennent à la classe, pas aux instances — accédés via ClassName.member. Utilisez static pour les constantes, fonctions utilitaires et méthodes de fabrique. Un constructeur privé + static getInstance() implémente le patron Singleton. 'as const' rend les tableaux statiques en lecture seule avec des types littéraux.

typescript
class Logger {
  static instance: Logger;
  private logs: string[] = [];

  private constructor() {} // prevent direct new

  static getInstance(): Logger {
    if (!Logger.instance) {
      Logger.instance = new Logger();
    }
    return Logger.instance;
  }

  static readonly LEVELS = ["INFO", "WARN", "ERROR"] as const;

  log(msg: string): void {
    this.logs.push(msg);
  }
}

const logger = Logger.getInstance();
console.log(Logger.LEVELS); // ["INFO", "WARN", "ERROR"]
// new Logger(); // Error: private constructor
07

Génériques

Fonctions génériques

Les génériques (<T>) vous permettent d'écrire des fonctions qui fonctionnent avec tout type tout en préservant la sécurité de type. Le paramètre de type T est un placeholder rempli au moment de l'appel — soit explicitement (identity<number>) soit inféré depuis les arguments. Les génériques permettent des structures de données et algorithmes réutilisables et de type sûr.

typescript
// Generic function: preserves type relationship
function identity<T>(value: T): T {
  return value;
}

const n = identity<number>(42);    // T = number, returns number
const s = identity("hi");          // T inferred as string

// Generic with multiple type params
function pair<A, B>(a: A, b: B): [A, B] {
  return [a, b];
}

const p = pair("Alice", 30); // [string, number]

// Generic with array
function first<T>(arr: T[]): T | undefined {
  return arr[0];
}

Classes génériques

Les classes génériques (<T>) créent des conteneurs de type sûr. Chaque instance verrouille un type spécifique — un Stack<number> n'accepte que des nombres. Cela capture les erreurs de type à la compilation sans surcoût à l'exécution (les génériques sont effacés). Courant dans les collections (Stack, Queue, Map) et les wrappers réactifs (Observable<T>).

typescript
class Stack<T> {
  private items: T[] = [];

  push(item: T): void {
    this.items.push(item);
  }

  pop(): T | undefined {
    return this.items.pop();
  }

  peek(): T | undefined {
    return this.items[this.items.length - 1];
  }

  get size(): number {
    return this.items.length;
  }
}

const numStack = new Stack<number>();
numStack.push(1);
numStack.push(2);
console.log(numStack.pop()); // 2

const strStack = new Stack<string>();
strStack.push("hello");

Contraintes génériques

Les contraintes (T extends SomeType) restreignent les types qu'un générique peut accepter. 'T extends HasLength' garantit que T a une propriété 'length'. 'K extends keyof T' (contrainte keyof) garantit qu'une clé existe sur un objet, renvoyant le type de valeur correct. Les contraintes permettent l'accès aux propriétés de type sûr et les appels de méthode sur les génériques.

typescript
// Constraint: T must have a 'length' property
interface HasLength {
  length: number;
}

function logLength<T extends HasLength>(item: T): void {
  console.log(item.length);
}

logLength("hello");     // 5 (string has length)
logLength([1, 2, 3]);   // 3 (array has length)
// logLength(42);        // Error: number has no length

// Constraint with keyof
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { name: "Alice", age: 30 };
const name = getProperty(user, "name"); // string
// getProperty(user, "email"); // Error: not a key

Paramètres de type par défaut

Les paramètres de type par défaut fournissent un type de secours lorsqu'aucun n'est spécifié. Utile pour les APIs avec un cas courant (ex. ApiResponse par défaut à string). Les valeurs par défaut peuvent dépendre des paramètres antérieurs. Combinez avec des contraintes (T extends X = DefaultType) pour des génériques optionnels de type sûr.

typescript
// Default type parameter
interface ApiResponse<T = string> {
  data: T;
  status: number;
}

const r1: ApiResponse = { data: "hello", status: 200 };       // T = string
const r2: ApiResponse<number> = { data: 42, status: 200 };    // T = number

// Multiple defaults
class Container<T = string, U = number> {
  constructor(public a: T, public b: U) {}
}

const c1 = new Container("hi", 42);      // Container<string, number>
const c2 = new Container<boolean>(true); // Container<boolean, number>

// Default with constraint
interface Box<T extends object = { id: number }> {
  value: T;
}

Interfaces & Types génériques

Les interfaces et alias de type génériques créent des contrats réutilisables et de type sûr. Repository<T> abstrait l'accès aux données avec une API cohérente. Result<T, E> est une union discriminée pour la gestion d'erreurs sans exceptions. Les paramètres de type par défaut (E = Error) réduisent le passe-partout pour les cas courants.

typescript
// Generic interface
interface Repository<T> {
  findById(id: string): Promise<T>;
  save(item: T): Promise<void>;
  delete(id: string): Promise<void>;
}

// Implement with concrete type
class UserRepo implements Repository<User> {
  async findById(id: string): Promise<User> { /* ... */ }
  async save(user: User): Promise<void> { /* ... */ }
  async delete(id: string): Promise<void> { /* ... */ }
}

// Generic type alias with conditional
type Result<T, E = Error> =
  | { ok: true; value: T }
  | { ok: false; error: E };

const success: Result<number> = { ok: true, value: 42 };
const failure: Result<string> = { ok: false, error: "not found" };

Types conditionnels

Les types conditionnels (T extends U ? X : Y) sont des if-statements au niveau du type. 'infer R' extrait un type depuis un autre type (ex., le type de retour d'une fonction). Les types conditionnels se distribuent sur les unions — ToArray<string | number> devient string[] | number[]. Les utilitaires intégrés comme Exclude, Extract et NonNullable utilisent cela.

typescript
// Conditional type: if T extends U, use X, else Y
type IsString<T> = T extends string ? true : false;
type A = IsString<"hi">;   // true
type B = IsString<42>;     // false

// Extract return type
type AsyncReturnType<T> =
  T extends (...args: any[]) => Promise<infer R> ? R : never;

async function fetchUser(): Promise<User> { /* ... */ }
type U = AsyncReturnType<typeof fetchUser>; // User

// Distributive conditional types
type ToArray<T> = T extends any ? T[] : never;
type R = ToArray<string | number>; // string[] | number[]

// Exclude / Extract built on conditionals
type NonNullable<T> = T extends null | undefined ? never : T;
08

Types avancés

Types utilitaires

TypeScript fournit des types utilitaires intégrés pour les transformations courantes : Partial (tout optionnel), Pick (sélectionner des clés), Omit (exclure des clés), Record (map clé-valeur), Required (supprimer optionnel), ReturnType (retour de fonction), Parameters (params de fonction comme tuple). Ils éliminent les définitions de type répétitives.

typescript
interface User {
  id: number;
  name: string;
  email: string;
  age: number;
}

// Partial: all optional (for updates)
type UserUpdate = Partial<User>;
const patch: UserUpdate = { name: "Bob" };

// Pick: select specific keys
type UserSummary = Pick<User, "id" | "name">;

// Omit: exclude specific keys
type CreateUser = Omit<User, "id">;

// Record: key-value map
type UserMap = Record<string, User>;

// Required: all required (remove ?)
type StrictUser = Required<Partial<User>>;

// ReturnType: function return type
type R = ReturnType<() => string>; // string

// Parameters: function parameter types as tuple
type P = Parameters<(a: number, b: string) => void>; // [number, string]

Types de littéraux de gabarit

Les types de littéraux de gabarit (TS 4.1+) construisent des types de chaîne en interpolant d'autres types. Combinés aux unions, ils génèrent des produits cartésiens de chaînes. Capitalize/Uppercase transforment la casse. Utilisez-les pour des noms d'événements de type sûr, la génération getter/setter et le typage de routes d'API. La clause 'as' dans les types mappés permet le renommage de clés.

typescript
// Build string types from other types
type Vertical = "top" | "bottom";
type Horizontal = "left" | "right";
type Position = `${Vertical}-${Horizontal}`;
// "top-left" | "top-right" | "bottom-left" | "bottom-right"

// Uppercase, Lowercase, Capitalize, Uncapitalize
type Upper = Uppercase<"hello">; // "HELLO"
type Cap = Capitalize<"foo">;    // "Foo"

// Getter names from keys
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

interface Person { name: string; age: number; }
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number; }

// Event listener types
type EventName = `on${Capitalize<"click" | "hover">}`;
// "onClick" | "onHover"

Mot-clé infer

Le mot-clé 'infer' déclare une variable de type dans la clause 'extends' d'un type conditionnel, capturant un type pour la réutilisation. C'est la fondation des types utilitaires comme ReturnType, Parameters et Awaited. Utilisez infer pour extraire des types de structures complexes (tableaux, promesses, fonctions) sans les décomposer manuellement.

typescript
// Extract return type of a function
type MyReturnType<T> =
  T extends (...args: any[]) => infer R ? R : never;

type R1 = MyReturnType<() => string>;    // string
type R2 = MyReturnType<(x: number) => boolean>; // boolean

// Extract element type of an array
type ElementOf<T> = T extends (infer E)[] ? E : never;
type E = ElementOf<string[]>;  // string

// Extract Promise value
type Unwrap<T> = T extends Promise<infer U> ? U : T;
type V = Unwrap<Promise<number>>; // number

// Extract first parameter
type FirstParam<T> =
  T extends (first: infer F, ...rest: any[]) => any ? F : never;
type FP = FirstParam<(name: string, age: number) => void>; // string

Unions discriminées

Les unions discriminées (unions étiquetées) utilisent un champ littéral partagé ('type', 'kind', 'tag') pour distinguer les variantes. TypeScript restreint le type dans chaque cas de switch, donnant accès aux champs spécifiques au cas. Le défaut 'never' active la vérification d'exhaustivité — si vous ajoutez un nouveau cas, le compilateur erreure jusqu'à ce que vous le gériez. Essentiel pour les reducers Redux et les machines à états.

typescript
// Discriminated union: shared 'type' (or 'kind') field
type Action =
  | { type: "ADD_TODO"; text: string }
  | { type: "DELETE_TODO"; id: number }
  | { type: "TOGGLE_TODO"; id: number };

function reducer(state: Todo[], action: Action): Todo[] {
  switch (action.type) {
    case "ADD_TODO":
      return [...state, { id: Date.now(), text: action.text }];
    case "DELETE_TODO":
      return state.filter(t => t.id !== action.id);
    case "TOGGLE_TODO":
      return state.map(t =>
        t.id === action.id ? { ...t, done: !t.done } : t
      );
    default:
      const _: never = action; // exhaustiveness check
      return state;
  }
}

Gardes de type : typeof & instanceof

Les gardes de type restreignent les types à l'exécution. 'typeof' fonctionne pour les primitifs (string, number, boolean, symbol, bigint, undefined, function, object). 'instanceof' vérifie les prototypes de classe/constructeur. Array.isArray() restreint à un tableau typé. Ce sont des gardes intégrés — pas de code personnalisé nécessaire. TypeScript suit le type restreint dans chaque branche.

typescript
// typeof: narrow primitives
function process(value: string | number) {
  if (typeof value === "string") {
    return value.toUpperCase(); // string
  }
  return value.toFixed(2);      // number
}

// instanceof: narrow class instances
class Dog { bark() {} }
class Cat { meow() {} }

function speak(pet: Dog | Cat) {
  if (pet instanceof Dog) {
    pet.bark(); // Dog
  } else {
    pet.meow(); // Cat
  }
}

// Array.isArray
function flatten(arr: (number | number[])[]) {
  return arr.flatMap(x =>
    Array.isArray(x) ? x : [x]
  );
}

Gardes de type personnalisés & Opérateur in

L'opérateur 'in' vérifie si une propriété existe sur un objet, restreignant au type qui l'a. Les prédicats de type (x is T) sont des fonctions de garde personnalisées qui renvoient un booléen mais restreignent aussi le type. Utilisez 'unknown' comme type d'entrée pour l'analyse sûre des données externes (JSON.parse, réponses d'API). Les prédicats permettent des vérifications de type réutilisables et composables.

typescript
// 'in' operator: check for property existence
interface Fish { swim(): void; }
interface Bird { fly(): void; }

function move(animal: Fish | Bird) {
  if ("swim" in animal) {
    animal.swim(); // Fish
  } else {
    animal.fly(); // Bird
  }
}

// Type predicate: custom guard function
function isString(x: unknown): x is string {
  return typeof x === "string";
}

function isUser(x: any): x is { name: string; age: number } {
  return x && typeof x.name === "string" && typeof x.age === "number";
}

const val: unknown = "hello";
if (isString(val)) {
  console.log(val.toUpperCase()); // narrowed to string
}
09

Structures de données

Tableaux & ReadonlyArray

Les tableaux TypeScript sont typés — les méthodes comme map, filter et reduce préservent les types d'éléments. Utilisez readonly T[] ou ReadonlyArray<T> pour l'immuabilité. Les tuples ont une longueur fixe et des positions typées. La déstructuration et le spread de tableau sont entièrement de type sûr. Le système de type capture les index hors plage et les affectations de mauvais type.

typescript
// Typed arrays
let nums: number[] = [1, 2, 3];
let strs: Array<string> = ["a", "b", "c"];

// ReadonlyArray (immutable)
const frozen: readonly number[] = [1, 2, 3];
// frozen.push(4); // Error: readonly

// Tuple (fixed length)
let pair: [string, number] = ["Alice", 30];

// Array methods are typed
const doubled = nums.map(n => n * 2);    // number[]
const evens = nums.filter(n => n % 2 === 0); // number[]
const sum = nums.reduce((a, b) => a + b, 0); // number

// Spread and destructuring
const [first, ...rest] = nums;
const combined = [...nums, ...[4, 5]];

Objets & Records

Record<K, V> crée une map typée avec des clés de type K et des valeurs de type V. Partial<T> rend toutes les propriétés optionnelles — idéal pour les opérations de mise à jour/patch. Object.entries/keys/values renvoient des tableaux typés. Utilisez Pick et Omit pour dériver des types focalisés depuis des existants, gardant les types DRY.

typescript
// Object type
const user: { name: string; age: number } = { name: "Alice", age: 30 };

// Record: typed key-value map
const scores: Record<string, number> = {
  math: 90,
  science: 85,
};

// Partial: all optional (for patches)
const patch: Partial<typeof user> = { age: 31 };

// Pick / Omit
type Summary = Pick<typeof user, "name">;
type WithoutAge = Omit<typeof user, "age">;

// Object.entries / keys / values typed
const entries = Object.entries(scores); // [string, number][]
const keys = Object.keys(scores);       // string[]
const values = Object.values(scores);   // number[]

Maps & Sets

Map et Set sont des collections ES6 avec plein support TypeScript. Les clés Map peuvent être de tout type (contrairement aux objets, qui forcent les clés en chaînes). Set stocke des valeurs uniques. WeakMap/WeakSet permettent le garbage collection des clés — utile pour les métadonnées attachées aux éléments DOM ou objets sans empêcher le GC.

typescript
// Map: keyed collection (any key type)
const map = new Map<string, User>();
map.set("alice", { name: "Alice", age: 30 });
const user = map.get("alice"); // User | undefined

// Set: unique values
const unique = new Set<number>([1, 2, 2, 3]);
console.log(unique.size); // 3
console.log(unique.has(2)); // true

// Iteration (typed)
for (const [key, value] of map) {
  console.log(key, value.name);
}

for (const num of unique) {
  console.log(num);
}

// WeakMap / WeakSet (keys must be objects, GC-friendly)
const weak = new WeakMap<object, string>();

Tuples & Tuples étiquetés

Les tuples sont des tableaux à longueur fixe avec des positions typées. Les tuples étiquetés (TS 4.0+) ajoutent des noms pour la lisibilité — utiles pour les valeurs de retour et les données de type CSV. Les tuples permettent des valeurs de retour multiples sans créer d'interface. Utilisez 'readonly' pour empêcher la mutation. Les tuples diffèrent des tableaux : [string, number] n'est PAS (string | number)[].

typescript
// Basic tuple
let point: [number, number] = [10, 20];

// Labeled tuple (TS 4.0+)
let user: [id: number, name: string, active: boolean] = [1, "Alice", true];

// Destructuring with labels
const [id, name, active] = user;

// Tuple in function returns
function divmod(a: number, b: number): [quotient: number, remainder: number] {
  return [Math.floor(a / b), a % b];
}

const [q, r] = divmod(17, 5);
console.log(q, r); // 3 2

// Readonly tuple
const fixed: readonly [string, number] = ["a", 1];
// fixed.push(2); // Error

Enums & Const enums

Les enums créent des constantes nommées. Les enums de chaîne sont recommandés (lisibles en sortie, pas de problèmes de mappage inverse). Les const enums sont effacés à la compilation (coût d'exécution nul). Pour les cas simples, les types union ('a' | 'b') sont souvent meilleurs — pas de code à l'exécution, meilleur tree-shaking. Utilisez les enums pour des constantes groupées et documentées.

typescript
// Numeric enum
enum Direction { North = 0, South = 1, East = 2, West = 3 }

// String enum (recommended)
enum HttpStatus {
  OK = "200 OK",
  NotFound = "404 Not Found",
  ServerError = "500 Internal Server Error",
}

// Const enum (inlined at compile time)
const enum Color { Red, Green, Blue }
let c = Color.Red; // compiles to: let c = 0;

// Union type alternative (no runtime code)
type Status = "pending" | "success" | "error";
const s: Status = "pending";

// Exhaustive switch
function handle(s: Status) {
  switch (s) {
    case "pending": return "loading";
    case "success": return "done";
    case "error": return "failed";
  }
}

Données immuables

TypeScript offre de multiples outils d'immuabilité : 'readonly' pour les propriétés, ReadonlyArray pour les tableaux, l'utilitaire Readonly<T> et 'as const' pour la lecture seule profonde avec des types littéraux. Les données immuables empêchent les mutations accidentelles et permettent la détection de changement (React, Redux). Utilisez le spread (...) pour des mises à jour immuables — crée un nouvel objet avec des champs modifiés.

typescript
// Readonly modifier
interface User {
  readonly id: number;
  name: string;
}

// ReadonlyArray
const nums: ReadonlyArray<number> = [1, 2, 3];
// nums[0] = 9; // Error

// Readonly utility type
type FrozenUser = Readonly<User>;

// as const: deep readonly with literal types
const config = {
  endpoint: "/api",
  methods: ["GET", "POST"],
} as const;
// config.endpoint: "/api" (literal, not string)
// config.methods: readonly ["GET", "POST"]

// Immutable update pattern
const user = { name: "Alice", age: 30 };
const updated = { ...user, age: 31 }; // new object
10

Modules & Espaces de noms

Modules ES : Import & Export

TypeScript utilise la syntaxe de module ES (import/export). Les exports nommés sont explicites ; l'export par défaut est le 'principal' export unique. Utilisez 'import * as' pour les imports d'espace de noms. La résolution de module suit les conventions Node.js (node_modules, extensions). Configurez 'module' et 'moduleResolution' dans tsconfig.json.

typescript
// math.ts - exporting
export function add(a: number, b: number): number {
  return a + b;
}

export const PI = 3.14159;

export default function multiply(a: number, b: number): number {
  return a * b;
}

// main.ts - importing
import multiply, { add, PI } from "./math";
import * as math from "./math"; // namespace import

console.log(add(1, 2));      // 3
console.log(multiply(3, 4)); // 12 (default)
console.log(math.PI);        // 3.14159

Imports de type uniquement

'import type' importe uniquement des types (effacés à la compilation, pas de code à l'exécution). Cela évite les dépendances circulaires et les imports d'exécution inutiles. TS 4.5+ permet les modificateurs 'type' en ligne dans les imports mixtes. Utilisez les imports de type uniquement pour les interfaces, alias de type et enums (si const) pour réduire la taille du bundle.

typescript
// Type-only import (erased at runtime)
import type { User, Config } from "./types";

// Mixed import (TS 4.5+)
import { render, type Component } from "./ui";

// Re-export types
export type { User } from "./types";

// Import type for interfaces and types
interface UserService {
  get(id: string): User;  // User from type-only import
}

// 'import type' ensures no runtime dependency
// Useful when the module has side effects you want to avoid

Imports dynamiques

Les imports dynamiques (import()) chargent les modules à la demande, renvoyant une Promesse. Cela permet le découpage de code et le chargement paresseux — critiques pour la performance dans les apps web. TypeScript infère le type de module automatiquement. Utilisez pour les fonctionnalités optionnelles, les grandes bibliothèques et le découpage de code basé sur les routes (React.lazy, Next.js dynamic).

typescript
// Dynamic import returns a Promise
async function loadModule() {
  const module = await import("./heavy-module");
  module.doWork();
}

// Conditional loading
if (featureEnabled) {
  const { Feature } = await import("./feature");
  new Feature().init();
}

// Type the dynamic import
type HeavyModule = typeof import("./heavy-module");

// With error handling
try {
  const lib = await import("./optional-lib");
  lib.run();
} catch (e) {
  console.log("Library not available");
}

Fichiers de déclaration & Augmentation de module

Les fichiers de déclaration (.d.ts) décrivent les types pour les modules JS, les imports CSS/PNG et les variables globales. L'augmentation de module étend les types de module existants — utile pour ajouter des propriétés à Express Request, Express Response ou des types tiers. C'est ainsi que le middleware comme passport ajoute le typage req.user.

typescript
// global.d.ts - declare ambient modules
declare module "*.css" {
  const content: string;
  export default content;
}

declare module "*.png" {
  const src: string;
  export default src;
}

// Module augmentation - extend existing types
import express from "express";

declare module "express" {
  interface Request {
    user?: { id: string; name: string };
  }
}

// Now req.user is typed
app.get("/", (req, res) => {
  console.log(req.user?.name);
});

Espaces de noms (Hérité)

Les espaces de noms sont le système de modules pré-ES6 de TypeScript. Ils regroupent le code lié sous un objet nommé. Pour les nouveaux projets, préférez les modules ES (import/export) — ils sont standardisés, tree-shakeables et fonctionnent avec les bundlers. Les espaces de noms restent utiles dans les fichiers de déclaration .d.ts pour les déclarations de type globales et le code hérité.

typescript
// Namespace: pre-ES6 way to organize code
namespace Validation {
  export interface StringValidator {
    isValid(s: string): boolean;
  }

  const lettersRegexp = /^[A-Za-z]+$/;

  export class LettersOnlyValidator implements StringValidator {
    isValid(s: string): boolean {
      return lettersRegexp.test(s);
    }
  }
}

// Usage
const validator = new Validation.LettersOnlyValidator();
console.log(validator.isValid("Hello")); // true

// Prefer ES modules over namespaces for new code
// Namespaces are useful in .d.ts files for global declarations

Paramètres de module tsconfig

Les paramètres de module tsconfig contrôlent comment TypeScript gère les imports. 'moduleResolution: node' utilise la résolution Node.js (lookup node_modules). 'esModuleInterop' active les imports par défaut depuis CommonJS. 'paths' crée des alias d'import (@/components) pour des imports plus propres. 'resolveJsonModule' permet d'importer des fichiers .json avec des types inférés.

typescript
{
  "compilerOptions": {
    "module": "ESNext",           // ES module output
    "moduleResolution": "node",   // Node-style resolution
    "esModuleInterop": true,      // allow default imports from CJS
    "allowSyntheticDefaultImports": true,
    "resolveJsonModule": true,    // import .json files
    "isolatedModules": true,      // each file is independent
    "baseUrl": "./src",           // base for non-relative imports
    "paths": {
      "@/*": ["./*"],             // path alias
      "@components/*": ["./components/*"]
    }
  }
}

// With paths config, you can import:
// import { Button } from "@/components/Button";
// instead of relative paths like "../../components/Button"
11

Async & Promesses

Types Promise

Promise<T> est le type async central — T est le type de valeur résolu. TypeScript infère les types à travers les chaînes .then(). Utilisez 'new Promise()' pour envelopper les APIs basés sur des rappels. Typez toujours les valeurs resolve/reject. Préférez async/await aux chaînes .then() brutes pour la lisibilité et la gestion d'erreurs.

typescript
// Promise<T> represents an async value
function fetchUser(id: number): Promise<User> {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (id === 1) {
        resolve({ id: 1, name: "Alice" });
      } else {
        reject(new Error("User not found"));
      }
    }, 100);
  });
}

// Consuming a Promise
fetchUser(1)
  .then(user => console.log(user.name))
  .catch(err => console.error(err))
  .finally(() => console.log("done"));

// Promise typing: then callbacks are typed
fetchUser(1).then(user => {
  // user is User, not any
  console.log(user.name); // string
});

async/await

async/await est du sucre syntaxique sur les Promesses — 'await' suspend jusqu'à ce que la Promesse se résolve. Les fonctions async renvoient toujours une Promesse. Utilisez Promise.all() pour l'exécution parallèle (beaucoup plus rapide que les awaits séquentiels). L'await de top niveau fonctionne dans les modules ES avec ES2022+. TypeScript vérifie que les valeurs attendues sont des Promesses.

typescript
// async function returns Promise<T>
async function getUser(id: number): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }
  return response.json() as Promise<User>;
}

// Top-level await (ES2022, TS 4.7+)
// const user = await getUser(1);

// Sequential vs parallel
async function loadAll() {
  // Sequential (slow)
  const a = await getUser(1);
  const b = await getUser(2);

  // Parallel (fast)
  const [x, y] = await Promise.all([getUser(1), getUser(2)]);
  return { a, b, x, y };
}

Gestion d'erreurs en Async

Utilisez try/catch avec async/await pour la gestion d'erreurs — c'est plus propre que .catch(). TypeScript ne prend pas en charge les throws typés (toutes les erreurs sont unknown dans catch), restreignez donc avec instanceof. Pour les erreurs prévisibles, envisagez le patron de type Result (union ok/erreur) au lieu des exceptions — cela rend la gestion d'erreurs explicite dans la signature de type.

typescript
// Try/catch with async/await
async function riskyOperation(): Promise<string> {
  try {
    const data = await fetch("/api/data");
    if (!data.ok) throw new Error(`HTTP ${data.status}`);
    return await data.text();
  } catch (error) {
    if (error instanceof Error) {
      console.error(error.message);
    }
    return "fallback";
  } finally {
    console.log("cleanup");
  }
}

// Typed errors (TypeScript doesn't have typed throws)
class ApiError extends Error {
  constructor(public status: number, message: string) {
    super(message);
  }
}

// Result type as alternative to exceptions
type Result<T> =
  | { ok: true; value: T }
  | { ok: false; error: string };

async function safeFetch(url: string): Promise<Result<string>> {
  try {
    const res = await fetch(url);
    return { ok: true, value: await res.text() };
  } catch (e) {
    return { ok: false, error: String(e) };
  }
}

Combinateurs de Promesse

Les combinateurs de Promesse orchestrent plusieurs opérations async : all() (parallèle, échec rapide), allSettled() (parallèle, attendre tout), race() (premier réglé), any() (premier réussi). Utilisez all() pour le chargement de données dépendant, allSettled() lorsque vous voulez des résultats partiels, race() pour les délais, any() pour les fetchs redondants.

typescript
// Promise.all: wait for all (rejects if any rejects)
const [users, posts] = await Promise.all([
  fetchUsers(),
  fetchPosts(),
]);

// Promise.allSettled: wait for all (never rejects)
const results = await Promise.allSettled([
  fetch("/api/a"),
  fetch("/api/b"),
]);
results.forEach(r => {
  if (r.status === "fulfilled") console.log(r.value);
  else console.log(r.reason);
});

// Promise.race: first to settle (resolve or reject)
const fastest = await Promise.race([
  fetch("/api/fast"),
  fetch("/api/slow"),
]);

// Promise.any: first to resolve (ignores rejections)
const first = await Promise.any([
  fetch("/api/primary"),
  fetch("/api/fallback"),
]);

Boucle d'événements & Microtâches

La boucle d'événements de JavaScript traite les microtâches (rappels de Promesse, queueMicrotask) avant les macrotâches (setTimeout, setInterval). C'est pourquoi les Promesses se résolvent avant les minuteurs. L'itération async (for await...of) consomme les itérables async — utile pour les flux. Les générateurs async (async function*) produisent des itérables async, permettant des séquences async paresseuses.

typescript
// Microtasks (Promises) run before macrotasks (setTimeout)
console.log("1: sync");

setTimeout(() => console.log("4: macrotask"), 0);

Promise.resolve().then(() => console.log("3: microtask"));

console.log("2: sync");
// Output: 1, 2, 3, 4

// queueMicrotask for custom microtasks
queueMicrotask(() => console.log("microtask"));

// Async iteration (for await)
async function processStream(stream: AsyncIterable<Buffer>) {
  for await (const chunk of stream) {
    console.log(chunk.length);
  }
}

// Async generators
async function* generate() {
  for (let i = 0; i < 3; i++) {
    await new Promise(r => setTimeout(r, 100));
    yield i;
  }
}

Patrons de concurrence

Ces patrons sont entièrement de type sûr en TypeScript. Debounce retarde l'exécution jusqu'à l'arrêt des appels pendant N ms (entrée de recherche). Throttle limite à un appel par N ms (gestionnaires de défilement). Semaphore/mapLimit contrôle la concurrence — utile pour les APIs à débit limité. Parameters<T> et ReturnType<T> préservent les signatures de fonction dans les wrappers.

typescript
// Debounce with TypeScript
function debounce<T extends (...args: any[]) => void>(
  fn: T,
  delay: number
): (...args: Parameters<T>) => void {
  let timer: ReturnType<typeof setTimeout>;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
}

// Throttle
function throttle<T extends (...args: any[]) => void>(
  fn: T,
  limit: number
): (...args: Parameters<T>) => void {
  let inThrottle = false;
  return (...args) => {
    if (!inThrottle) {
      fn(...args);
      inThrottle = true;
      setTimeout(() => (inThrottle = false), limit);
    }
  };
}

// Semaphore (limit concurrency)
async function mapLimit<T, U>(
  items: T[],
  limit: number,
  fn: (item: T) => Promise<U>
): Promise<U[]> {
  const results: U[] = [];
  const executing: Promise<void>[] = [];
  for (const item of items) {
    const p = fn(item).then(r => results.push(r));
    executing.push(p);
    if (executing.length >= limit) {
      await Promise.race(executing);
      executing.splice(executing.findIndex(e => e === p), 1);
    }
  }
  await Promise.all(executing);
  return results;
}
12

Gestion d'erreurs & Tests

Try/Catch avec Unknown

Depuis TypeScript 4.4 (useUnknownInCatchVariables), les erreurs capturées sont 'unknown' — vous devez les restreindre avant usage. Cela empêche d'accéder à des propriétés qui n'existent pas. Utilisez instanceof pour vérifier des types d'erreur spécifiques, ou String() comme repli. Créez un assistant getErrorMessage() pour une extraction d'erreur cohérente.

typescript
// In strict mode, catch is 'unknown' (not 'any')
try {
  JSON.parse("invalid");
} catch (error: unknown) {
  // Must narrow before using
  if (error instanceof SyntaxError) {
    console.error("JSON error:", error.message);
  } else if (error instanceof Error) {
    console.error(error.message);
  } else {
    console.error("Unknown error:", error);
  }
}

// Helper function
function getErrorMessage(error: unknown): string {
  if (error instanceof Error) return error.message;
  return String(error);
}

Classes d'erreur personnalisées

Les classes d'erreur personnalisées ajoutent des données structurées (code, statusCode, champ) aux erreurs. Appelez toujours super(message) et définissez le prototype (Object.setPrototypeOf) pour corriger le problème de chaîne de prototype TypeScript/ES5. Utilisez instanceof pour distinguer les types d'erreur dans les blocs catch. Ce patron est essentiel pour le middleware Express et les gestionnaires d'erreur d'API.

typescript
class AppError extends Error {
  constructor(
    message: string,
    public code: string,
    public statusCode: number = 500
  ) {
    super(message);
    this.name = "AppError";
    // Fix prototype chain (TS quirk)
    Object.setPrototypeOf(this, AppError.prototype);
  }
}

class ValidationError extends AppError {
  constructor(message: string, public field: string) {
    super(message, "VALIDATION_ERROR", 400);
    this.name = "ValidationError";
  }
}

// Usage
function createUser(input: unknown) {
  if (typeof input !== "object" || input === null) {
    throw new ValidationError("Invalid input", "body");
  }
}

try {
  createUser("bad");
} catch (e) {
  if (e instanceof ValidationError) {
    console.log(e.field, e.statusCode); // "body" 400
  }
}

Patron de type Result

Le type Result (de Rust) rend les erreurs explicites dans la signature de type — les appelants doivent gérer à la fois le succès et l'échec. Contrairement aux exceptions, le compilateur applique la gestion d'erreurs. Utilisez cela pour les échecs attendus (validation, non-trouvé) où les exceptions seraient excessives. Réservez les exceptions pour les erreurs véritablement inattendues (bugs, défaillances système).

typescript
// Result type: explicit error handling without exceptions
type Result<T, E = string> =
  | { ok: true; value: T }
  | { ok: false; error: E };

function divide(a: number, b: number): Result<number> {
  if (b === 0) {
    return { ok: false, error: "Division by zero" };
  }
  return { ok: true, value: a / b };
}

// Usage: forced to handle both cases
const result = divide(10, 0);
if (result.ok) {
  console.log(result.value); // number
} else {
  console.error(result.error); // string
}

// Utility helpers
function ok<T>(value: T): Result<T, never> {
  return { ok: true, value };
}

function err<E>(error: E): Result<never, E> {
  return { ok: false, error };
}

Fonctions d'assertion

Les fonctions d'assertion (asserts X) lèvent une exception si une condition échoue ET restreignent le type ensuite. 'asserts value is string' dit à TypeScript qu'après l'appel, value est string. C'est plus propre que des if-checks répétés. Utilisez pour la validation à l'exécution aux frontières (entrée d'API, config). Combinez avec Zod ou io-ts pour la validation de schéma.

typescript
// Assertion function: throws if condition is false
function assert(condition: unknown, message: string): asserts condition {
  if (!condition) {
    throw new Error(message);
  }
}

// Assert value is a specific type
function assertString(value: unknown): asserts value is string {
  if (typeof value !== "string") {
    throw new Error(`Expected string, got ${typeof value}`);
  }
}

// Usage: narrows type after assertion
const input: unknown = "hello";
assertString(input);
console.log(input.toUpperCase()); // input is now string

// Assert non-null
function assertDefined<T>(value: T | undefined): asserts value is T {
  if (value === undefined) throw new Error("undefined");
}

Analyse JSON de type sûr

JSON.parse renvoie 'any' — non sûr. Enveloppez-le avec une garde de type pour valider la forme à l'exécution et restreindre le type. Pour les schémas complexes, utilisez Zod, io-ts ou yup — ils génèrent à la fois les validateurs à l'exécution et les types TypeScript depuis une seule définition de schéma. C'est critique pour les réponses d'API et l'entrée utilisateur.

typescript
// Safe JSON parse with type guard
function safeParse<T>(json: string, guard: (x: unknown) => x is T): T | null {
  try {
    const parsed: unknown = JSON.parse(json);
    if (guard(parsed)) return parsed;
    return null;
  } catch {
    return null;
  }
}

// Type guard for User
interface User { id: number; name: string; }
function isUser(x: unknown): x is User {
  return typeof x === "object" && x !== null
    && typeof (x as User).id === "number"
    && typeof (x as User).name === "string";
}

const data = safeParse('{"id":1,"name":"Alice"}', isUser);
if (data) {
  console.log(data.name); // User
}

// Using Zod for runtime validation
// import { z } from "zod";
// const UserSchema = z.object({ id: z.number(), name: z.string() });
// const user = UserSchema.parse(JSON.parse(json));

Vérification d'exhaustivité

La vérification d'exhaustivité garantit que vous gérez tous les cas d'une union. Affectez le cas par défaut à 'never' — si vous ajoutez une nouvelle variante à l'union, TypeScript erreure car le nouveau type n'est pas assignable à 'never'. Cela capture les cas manquants à la compilation. Essentiel pour les unions discriminées, les reducers Redux et les machines à états.

typescript
type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; size: number }
  | { kind: "rectangle"; w: number; h: number };

function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.size ** 2;
    case "rectangle":
      return shape.w * shape.h;
    default:
      // If a new shape is added, this errors
      const _exhaustive: never = shape;
      return _exhaustive;
  }
}

// Adding a new variant:
// | { kind: "triangle"; base: number; height: number }
// Now the default case errors: Type 'triangle' is not assignable to 'never'
13

Décorateurs & Métadonnées

Décorateurs de classe

Les décorateurs de classe reçoivent la fonction constructeur et peuvent renvoyer une classe modifiée. Les fabriques de décorateurs (renvoyant une fonction) acceptent des arguments. Les décorateurs sont une fonctionnalité expérimentale — activez 'experimentalDecorators' dans tsconfig. Largement utilisés dans NestJS, TypeORM et Angular pour l'injection de dépendances et les métadonnées.

typescript
// Class decorator: receives the constructor
function Logged<T extends new (...args: any[]) => any>(target: T): T {
  return class extends target {
    constructor(...args: any[]) {
      super(...args);
      console.log(`Created ${target.name}`);
    }
  };
}

@Logged
class Service {
  constructor(public name: string) {}
}

const s = new Service("Auth");
// Logs: "Created Service"

// Decorator factory (with arguments)
function Prefix(prefix: string) {
  return function <T extends new (...args: any[]) => any>(target: T): T {
    return class extends target {
      message = prefix + " " + (this as any).name;
    };
  };
}

Décorateurs de méthode & propriété

Les décorateurs de méthode reçoivent (target, propertyKey, descriptor) et peuvent envelopper la méthode originale — utile pour la journalisation, le cache et le contrôle d'accès. Les décorateurs de propriété reçoivent (target, key) et sont souvent utilisés pour enregistrer des métadonnées. Le descriptor.value est la fonction originale ; enveloppez-la pour ajouter du comportement. Courant dans NestJS (@Get, @Post) et TypeORM (@Column).

typescript
// Method decorator: (target, key, descriptor)
function Log(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const original = descriptor.value;
  descriptor.value = function (...args: any[]) {
    console.log(`Calling ${propertyKey}(${args})`);
    return original.apply(this, args);
  };
}

class Calculator {
  @Log
  add(a: number, b: number): number {
    return a + b;
  }
}

new Calculator().add(2, 3);
// Logs: "Calling add(2,3)"

// Property decorator: (target, key)
function Required(target: any, key: string) {
  // Store metadata for validation
  console.log(`Required: ${key}`);
}

Décorateurs de paramètre & Métadonnées

Les décorateurs de paramètre reçoivent (target, key, index) et sont utilisés pour l'injection de dépendances (NestJS, Angular). Le polyfill 'reflect-metadata' active les métadonnées de type à l'exécution — les décorateurs peuvent accéder aux types de paramètres via Reflect.getMetadata('design:paramtypes'). C'est ainsi que les conteneurs DI savent quoi injecter. Activez avec 'emitDecoratorMetadata: true' dans tsconfig.

typescript
import "reflect-metadata";

// Parameter decorator
function Inject(target: any, key: string, index: number) {
  console.log(`Inject param ${index} of ${key}`);
}

class Service {
  constructor(@Inject private dep: any) {}
}

// Store and retrieve metadata
const METADATA_KEY = "design:type";

class Example {
  greet(name: string): void {}
}

// reflect-metadata provides:
// - design:type (property type)
// - design:paramtypes (method parameter types)
// - design:returntype (method return type)

const types = Reflect.getMetadata("design:paramtypes", Example.prototype, "greet");
// types: [String]

Décorateurs d'accesseur

Les décorateurs d'accesseur s'appliquent aux getters/setters. Le descriptor a des propriétés get/set que vous pouvez envelopper. Utilisez-les pour la validation, la journalisation ou le changement d'énumérabilité. Le patron de validation (MaxLength, Min, Max) enveloppe le setter pour appliquer des contraintes à l'exécution. C'est ainsi que class-validator (NestJS) fonctionne pour la validation de DTO.

typescript
// Accessor decorator (getter/setter)
function Enumerable(value: boolean) {
  return function (
    target: any,
    key: string,
    descriptor: PropertyDescriptor
  ) {
    descriptor.enumerable = value;
  };
}

class Person {
  private _name: string = "";

  @Enumerable(true)
  get name(): string {
    return this._name;
  }

  set name(value: string) {
    this._name = value;
  }
}

const p = new Person();
p.name = "Alice";
console.log(Object.keys(p)); // ["name"] (enumerable)

// Validation decorator
function MaxLength(len: number) {
  return function (target: any, key: string) {
    let value: string;
    Object.defineProperty(target, key, {
      get() { return value; },
      set(v: string) {
        if (v.length > len) throw new Error("Too long");
        value = v;
      },
    });
  };
}

Décorateurs modernes (TC39 Stage 3)

TypeScript 5.0 prend en charge la proposition de décorateur TC39 Stage 3 — une API standardisée remplaçant experimentalDecorators. La nouvelle API utilise un objet de contexte (ClassMethodDecoratorContext) au lieu de (target, key, descriptor). C'est plus propre, de type sûr, et finira par être dans le standard JS. Utilisez cela pour les nouveaux projets ; les décorateurs expérimentaux restent pour la compatibilité NestJS/Angular.

typescript
// TC39 Stage 3 decorators (TS 5.0+, no experimentalDecorators needed)
// Uses a different API: context object

function log<This, Args extends any[], Return>(
  target: (this: This, ...args: Args) => Return,
  context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
) {
  return function (this: This, ...args: Args): Return {
    console.log(`Calling ${String(context.name)}`);
    return target.call(this, ...args);
  };
}

class Service {
  @log
  greet(name: string): string {
    return `Hello, ${name}`;
  }
}

// Class method decorator context provides:
// - name: property name
// - kind: "method" | "field" | "class"
// - access: { get(), set(value) } for fields
// - addInitializer(): run code after construction

Décorateur pratique : Memoize

Ce décorateur memoize met en cache les résultats de méthode basés sur les arguments — accélérations spectaculaires pour les fonctions pures coûteuses comme fibonacci. Le cache est par instance (utilisez un WeakMap pour un cache partagé). Les décorateurs brillent pour les préoccupations transversales : journalisation, cache, validation, contrôle d'accès, logique de réessai. Ils gardent la logique métier propre en séparant les préoccupations d'infrastructure.

typescript
// Memoize: cache method results
function Memoize<This, Args extends any[], Return>(
  target: (this: This, ...args: Args) => Return,
  context: ClassMethodDecoratorContext
) {
  const cache = new Map<string, Return>();
  return function (this: This, ...args: Args): Return {
    const key = JSON.stringify(args);
    if (cache.has(key)) return cache.get(key)!;
    const result = target.call(this, ...args);
    cache.set(key, result);
    return result;
  };
}

class MathService {
  @Memoize
  fibonacci(n: number): number {
    if (n < 2) return n;
    return this.fibonacci(n - 1) + this.fibonacci(n - 2);
  }
}

const svc = new MathService();
console.time("first");
console.log(svc.fibonacci(40)); // slow
console.timeEnd("first");

console.time("second");
console.log(svc.fibonacci(40)); // fast (cached)
console.timeEnd("second");
14

Types utilitaires

Partial, Required & Readonly

Partial<T> rend toutes les propriétés optionnelles — parfait pour les opérations de mise à jour/patch où seuls certains champs changent. Required<T> est l'inverse. Readonly<T> rend toutes les propriétés immuables à la compilation. Ce sont les types utilitaires les plus couramment utilisés et éliminent le besoin de maintenir des interfaces optionnelles/en-lecture-seule parallèles manuellement.

typescript
interface User {
  id: number;
  name: string;
  email: string;
}

// Partial<T>: all properties become optional
type UserUpdate = Partial<User>;
// { id?: number; name?: string; email?: string }

function updateUser(id: number, changes: UserUpdate) {
  // Only provided fields are updated
  Object.assign(users[id], changes);
}
updateUser(1, { name: "Alice" }); // OK — only name

// Required<T>: all properties become required (inverse of Partial)
type StrictUser = Required<Partial<User>>; // back to all required

// Readonly<T>: all properties become readonly
type FrozenUser = Readonly<User>;
const frozen: FrozenUser = { id: 1, name: "Alice", email: "[email protected]" };
// frozen.name = "Bob"; // Error: readonly

// Practical: immutable config objects
const config: Readonly<Config> = { /* ... */ };
// config.port = 8081; // Error — prevents accidental mutation

Pick, Omit & Record

Pick<T, K> extrait un sous-ensemble de propriétés ; Omit<T, K> supprime des propriétés — les deux créent des types dérivés sans duplication. Record<K, V> crée un type de map/dictionnaire avec des clés spécifiques. Ce sont essentiels pour les DTOs (objets de transfert de données) : dérivez un type CreateUser depuis User en omettant les champs auto-générés comme id et createdAt. Cela garde les types DRY et synchronisés.

typescript
interface User {
  id: number;
  name: string;
  email: string;
  role: string;
  createdAt: Date;
}

// Pick<T, Keys>: select specific properties
type UserSummary = Pick<User, "id" | "name">;
// { id: number; name: string }

// Omit<T, Keys>: remove specific properties
type UserInput = Omit<User, "id" | "createdAt">;
// { name: string; email: string; role: string }

// Record<Keys, Value>: object with specific keys and value type
type UserRole = "admin" | "user" | "guest";
type Permissions = Record<UserRole, string[]>;
const perms: Permissions = {
  admin: ["read", "write", "delete"],
  user: ["read", "write"],
  guest: ["read"],
};

// Combining: create a DTO from a full entity
type UserDTO = Pick<User, "id" | "name" | "email">;
type CreateUserDTO = Omit<User, "id" | "createdAt">;

ReturnType, Parameters & Awaited

ReturnType et Parameters extraient les types depuis des fonctions existantes — inestimables lors de l'enveloppement ou de l'appel de fonctions dont vous ne voulez pas dupliquer les signatures. Awaited<T> déballote les Promesses imbriquées (Promise<Promise<T>> devient T), essentiel pour les types de retour de fonction async. InstanceType obtient le type d'instance depuis un constructeur de classe. Ils permettent la composition de fonction de type sûr et les utilitaires d'ordre supérieur.

typescript
function fetchUser(id: number): Promise<{ name: string; age: number }> {
  return Promise.resolve({ name: "Alice", age: 30 });
}

// ReturnType<T>: the return type of a function
type FetchResult = ReturnType<typeof fetchUser>;
// Promise<{ name: string; age: number }>

// Awaited<T>: unwrap a Promise to its inner type
type User = Awaited<ReturnType<typeof fetchUser>>;
// { name: string; age: number }

// Parameters<T>: tuple of parameter types
type FetchParams = Parameters<typeof fetchUser>;
// [id: number]

// First parameter type
type FirstParam = Parameters<typeof fetchUser>[0]; // number

// ConstructorParameters<T>: parameters of a class constructor
class Point {
  constructor(public x: number, public y: number) {}
}
type PointArgs = ConstructorParameters<typeof Point>; // [x: number, y: number]

// InstanceType<T>: the instance type of a constructor
type PointInstance = InstanceType<typeof Point>; // Point

Exclude, Extract & NonNullable

Exclude<T, U> supprime des types d'une union ; Extract<T, U> ne garde que les types correspondants — les deux opèrent sur les membres d'union. NonNullable<T> supprime null et undefined. Ce sont des blocs de construction : Omit est défini comme Pick<T, Exclude<keyof T, K>>. Utilisez Exclude/Extract pour filtrer les types d'union dynamiquement, ex. séparer les types d'erreur des types de succès dans une union de résultat.

typescript
type Role = "admin" | "user" | "guest" | "superadmin";

// Exclude<T, U>: remove types from a union
type NonAdmin = Exclude<Role, "admin" | "superadmin">;
// "user" | "guest"

// Extract<T, U>: keep only matching types from a union
type AdminRoles = Extract<Role, "admin" | "superadmin">;
// "admin" | "superadmin"

// NonNullable<T>: remove null and undefined
type MaybeUser = User | null | undefined;
type DefiniteUser = NonNullable<MaybeUser>; // User

// Practical: filter union types
type EventMap = {
  click: MouseEvent;
  keydown: KeyboardEvent;
  scroll: UIEvent;
};
type EventName = keyof EventMap; // "click" | "keydown" | "scroll"

// Omit<T, K> is actually built from Pick and Exclude:
// type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;

Types utilitaires personnalisés

Les types utilitaires personnalisés composent les intégrés pour des besoins spécifiques. Optional<T, K> rend uniquement certains champs optionnels (plus ciblé que Partial). DeepPartial/DeepReadonly s'appliquent récursivement aux objets imbriqués — utiles pour la config et les arbres d'état. Le modificateur -readonly dans Mutable supprime readonly. Ces patrons montrent comment les types mappés et conditionnels se combinent pour une programmation de type puissante.

typescript
// Make specific properties optional
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

interface User {
  id: number;
  name: string;
  email: string;
}
type UserWithOptionalEmail = Optional<User, "email">;
// { id: number; name: string; email?: string }

// Make specific properties required
type Require<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;

// Deep partial (recursively make all properties optional)
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

// Deep readonly
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};

// Mutable (remove readonly from all properties)
type Mutable<T> = {
  -readonly [P in keyof T]: T[P];
};

// Non-never keys (filter out never-valued properties)
type NonNeverKeys<T> = {
  [K in keyof T]: T[K] extends never ? never : K;
}[keyof T];
15

Types conditionnels

Types conditionnels de base (T extends U ? X : Y)

Les types conditionnels (T extends U ? X : Y) sélectionnent un type basé sur une condition au niveau du type — comme un ternaire pour les types. Ils sont la fondation de la programmation de type de TypeScript. Lorsque T est une union, la condition se distribue sur chaque membre (types conditionnels distributifs). Le mot-clé infer extrait des types depuis un motif, comme tirer le type d'élément d'un tableau ou le type de résolution d'une Promesse.

typescript
// Conditional types choose a type based on a condition
// Syntax: T extends U ? X : Y

// Is T an array? Return its element type, else never
type ElementOf<T> = T extends (infer E)[] ? E : never;

type A = ElementOf<string[]>;    // string
type B = ElementOf<number[]>;    // number
type C = ElementOf<string>;      // never (not an array)

// Is T a Promise? Unwrap it, else keep T
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

type D = UnwrapPromise<Promise<number>>;  // number
type E = UnwrapPromise<string>;           // string (not a promise)

// Check if a type is a function
type IsFunction<T> = T extends (...args: any[]) => any ? true : false;

type F = IsFunction<() => void>;  // true
type G = IsFunction<number>;      // false

// Conditional types are evaluated lazily and distribute over unions

Mot-clé infer (Extraction de type)

Le mot-clé infer déclare une variable de type dans la clause extends d'un type conditionnel, capturant tout type correspondant à cette position. C'est comment ReturnType, Parameters et Awaited sont implémentés. infer peut être utilisé récursivement (Unwrap<Promise<Promise<T>>>) pour déballoter entièrement les types imbriqués. C'est l'outil principal pour extraire des types de structures génériques complexes.

typescript
// infer extracts a type from within a pattern

// Get the return type of a function (like ReturnType<T>)
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

// Get the first parameter type
type FirstArg<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;
type FA = FirstArg<(name: string, age: number) => void>; // string

// Get the resolved value of a Promise (like Awaited)
type Unwrap<T> = T extends Promise<infer U> ? Unwrap<U> : T;
type Deep = Unwrap<Promise<Promise<Promise<number>>>>; // number (recursive!)

// Extract the value type from a Map
type MapValue<M> = M extends Map<any, infer V> ? V : never;
type MV = MapValue<Map<string, number>>; // number

// Extract element type from Set
type SetElement<S> = S extends Set<infer E> ? E : never;

// Get the instance type from a constructor
type Instance<T> = T extends new (...args: any[]) => infer I ? I : never;

Types conditionnels distributifs

Les types conditionnels se distribuent sur les unions : appliquer ToArray<A | B> donne ToArray<A> | ToArray<B>, pas (A | B)[]. C'est comment Exclude et NonNullable filtrent les membres d'union — ils renvoient 'never' pour les types exclus, qui s'effondre dans l'union. Pour empêcher la distribution, enveloppez les deux côtés entre crochets : [T] extends [U]. La distribution est généralement ce que vous voulez pour le filtrage, mais la non-distributive est nécessaire pour les opérations 'envelopper toute l'union'.

typescript
// Conditional types DISTRIBUTE over unions
// T extends U ? X : Y  applied to  A | B  becomes
// (A extends U ? X : Y) | (B extends U ? X : Y)

type ToArray<T> = T extends any ? T[] : never;

type Result = ToArray<string | number>;
// ToArray<string> | ToArray<number>
// = string[] | number[]

// WITHOUT distribution (wrap in brackets to prevent):
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type Result2 = ToArrayNonDist<string | number>;
// (string | number)[]  — a single array of the union

// Practical: filter out types from a union
type ExcludeNull<T> = T extends null | undefined ? never : T;
type Cleaned = ExcludeNull<string | null | number | undefined>;
// string | number  (null and undefined filtered out)

// This is exactly how NonNullable<T> works:
// type NonNullable<T> = T extends null | undefined ? never : T;

Contraintes de type conditionnel

Les types conditionnels peuvent être imbriqués pour créer une discrimination au niveau du type (comme une instruction switch pour les types). Combinés avec infer, ils extraient et dérivent des types de paramètres génériques. C'est comment les bibliothèques comme React dérivent les types de props depuis les définitions de composant, et comment les bibliothèques de routage extraient les types de paramètres depuis les chaînes de chemin. La contrainte (T extends any[]) garantit que l'entrée est valide avant que le conditionnel n'extraie le type d'élément.

typescript
// Use conditional types to constrain and derive types

// Get the props type of a React component
type PropsOf<C> = C extends React.ComponentType<infer P> ? P : never;
type ButtonProps = PropsOf<typeof Button>; // the component's prop type

// Conditional with multiple branches (nested)
type TypeName<T> =
  T extends string ? "string" :
  T extends number ? "number" :
  T extends boolean ? "boolean" :
  T extends undefined ? "undefined" :
  T extends Function ? "function" :
  "object";

type T1 = TypeName<string>;     // "string"
type T2 = TypeName<() => void>; // "function"
type T3 = TypeName<{}>;         // "object"

// Constraint + conditional: only allow arrays, get element type
function firstElement<T extends any[]>(arr: T): T extends (infer E)[] ? E : never {
  return arr[0] as any;
}
const x: number = firstElement([1, 2, 3]); // number

Types de littéraux de gabarit (Manipulation de chaîne)

Les types de littéraux de gabarit permettent la manipulation de chaîne au niveau du type — concaténation, conversion de casse et correspondance de motifs. Combinés aux types conditionnels et infer, ils peuvent analyser les chaînes de chemin pour extraire les paramètres de route, générer des noms de gestionnaire d'événements ou construire des accesseurs de propriété de type sûr. C'est comment les frameworks comme Next.js et tRPC créent des APIs de type sûr de bout en bout depuis des littéraux de chaîne.

typescript
// Template literal types: type-level string operations

type Greeting = `Hello ${string}`;
const g: Greeting = "Hello World"; // OK
// const bad: Greeting = "Hi World"; // Error

// Uppercase, Lowercase, Capitalize, Uncapitalize
type Upper = Uppercase<"hello">; // "HELLO"
type Lower = Lowercase<"WORLD">; // "world"
type Cap = Capitalize<"foo">;    // "Foo"

// Build event handler names from event names
type EventName = "click" | "focus" | "blur";
type HandlerName = `on${Capitalize<EventName>}`;
// "onClick" | "onFocus" | "onBlur"

// Extract route parameters
type ExtractParams<Path extends string> =
  Path extends `${infer _Start}/${infer Param}/${infer Rest}`
    ? Param | ExtractParams<`/${Rest}`>
    : Path extends `${infer _Start}/${infer Param}`
    ? Param
    : never;

type Params = ExtractParams<"/users/:id/posts/:postId">;
// ":id" | ":postId"

// Property accessor type: "a.b.c" -> nested type
type Get<T, P extends string> =
  P extends `${infer Key}.${infer Rest}`
    ? Key extends keyof T ? Get<T[Key], Rest> : never
    : P extends keyof T ? T[P] : never;
16

Types mappés

Types mappés de base

Les types mappés itèrent sur les clés d'un objet et transforment chaque propriété — [K in keyof T] est la syntaxe. C'est comment Partial, Readonly, Pick et autres types utilitaires sont implémentés. Vous pouvez modifier le type de propriété (T[K] | null), ajouter des modificateurs (? ou readonly), ou remplacer complètement le type de valeur. Les types mappés sont l'épine dorsale du système de transformation de type de TypeScript.

typescript
// Mapped types transform each property of an existing type

// Make all properties optional (like Partial<T>)
type MyPartial<T> = {
  [K in keyof T]?: T[K];
};

// Make all properties readonly (like Readonly<T>)
type MyReadonly<T> = {
  readonly [K in keyof T]: T[K];
};

// Make all properties nullable
type Nullable<T> = {
  [K in keyof T]: T[K] | null;
};

interface User {
  id: number;
  name: string;
  email: string;
}

type NullableUser = Nullable<User>;
// { id: number | null; name: string | null; email: string | null }

// Change all property types to a specific type
type Stringify<T> = {
  [K in keyof T]: string;
};
type StringUser = Stringify<User>;
// { id: string; name: string; email: string }

Remappage de clés via 'as'

Le remappage de clés (clause as, TS 4.1+) vous permet de renommer ou filtrer les clés pendant le mappage. Utilisez les types de littéraux de gabarit pour transformer les noms de clés (ajouter des préfixes, convertir en getters, majuscules). Renvoyer 'never' pour une clé la supprime — c'est comment vous filtrez les propriétés. Combiné aux types conditionnels, le remappage de clés permet des transformations puissantes comme convertir un schéma de données en schéma de validation ou un type d'API en type de formulaire.

typescript
// Remap keys using 'as' (TypeScript 4.1+)

// Add a prefix to all keys
type Prefix<T, P extends string> = {
  [K in keyof T as `${P}${Capitalize<string & K>}`]: T[K];
};

interface User {
  id: number;
  name: string;
}
type PrefixedUser = Prefix<User, "user">;
// { userId: number; userName: string }

// Convert keys to getters
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type UserGetters = Getters<User>;
// { getId: () => number; getName: () => string }

// Filter out keys by returning never
type RemoveKindField<T> = {
  [K in keyof T as Exclude<K, "kind">]: T[K];
};
type WithoutKind = RemoveKindField<{ kind: "circle"; radius: number }>;
// { radius: number }

// Make keys UPPERCASE
type UpperKeys<T> = {
  [K in keyof T as Uppercase<string & K>]: T[K];
};

Modificateurs : +, -, ?, readonly

Les modificateurs + et - ajoutent ou suppriment des modificateurs de propriété. -? supprime l'optionalité (rendant les champs optionnels requis) ; -readonly supprime l'immuabilité. C'est comment Required<T> et le patron Mutable fonctionnent. Le préfixe + est optionnel (readonly est identique à +readonly), mais - est requis pour la suppression. Cela donne un contrôle fin sur les caractéristiques de propriété pendant les transformations de type.

typescript
// Add (+) or remove (-) modifiers

// Remove optional (?) modifier: -?
type Concrete<T> = {
  [K in keyof T]-?: T[K];
};

interface OptionalUser {
  id?: number;
  name?: string;
}
type RequiredUser = Concrete<OptionalUser>;
// { id: number; name: string } — all required now

// Remove readonly modifier: -readonly
type Mutable<T> = {
  -readonly [K in keyof T]: T[K];
};

interface FrozenConfig {
  readonly port: number;
  readonly host: string;
}
type EditableConfig = Mutable<FrozenConfig>;
// { port: number; host: string } — mutable now

// Add readonly modifier: +readonly (or just readonly)
type Freeze<T> = {
  +readonly [K in keyof T]: T[K];
};

// Add optional modifier: +?
type MakeOptional<T> = {
  [K in keyof T]+?: T[K];
};

Types mappés homomorphes

Les types mappés homomorphes ([K in keyof T]) préservent les modificateurs de propriété (readonly, ?) du type source — c'est pourquoi Pick<User, 'id'> garde id readonly. Les mappages non-homomorphes (ex. [K in string]) ne préservent pas les modificateurs. Cela compte lors de la dérivation de types : un Partial homomorphe d'un type avec des champs readonly garde ces champs readonly (mais optionnels). Comprendre l'homomorphisme aide à prédire si les modificateurs survivent à une transformation.

typescript
// Homomorphic mapped types preserve modifiers from the original type

interface User {
  readonly id: number;
  name: string;
  email?: string;
}

// Homomorphic: [K in keyof T] — preserves readonly and ?
type Clone<T> = {
  [K in keyof T]: T[K];
};
type ClonedUser = Clone<User>;
// { readonly id: number; name: string; email?: string }
// Modifiers are PRESERVED from User

// Non-homomorphic: doesn't preserve modifiers
type NonHomo<T> = {
  [K in string]: T; // not keyof T — loses modifiers
};

// Pick is homomorphic — preserves modifiers
type UserPick = Pick<User, "id" | "name">;
// { readonly id: number; name: string } — readonly preserved!

// Practical: create a "patch" type that preserves optionality
type Patch<T> = {
  [K in keyof T]?: T[K]; // Partial is homomorphic
};

Construire un type de validation depuis un schéma

C'est comment les bibliothèques de formulaire (React Hook Form, Formik) et de validation (Zod, Yup) maintiennent la sécurité de type — elles dérivent les validateurs et types de formulaire depuis vos interfaces de données en utilisant des types mappés. Lorsque vous ajoutez un champ à User, le validateur et les types de formulaire l'exigent automatiquement aussi, empêchant la dérive. Cela démontre la puissance réelle des types mappés : une source de vérité (l'interface) pilote plusieurs types dérivés.

typescript
// Practical: derive a validator type from a data type

// Given a data interface
interface User {
  id: number;
  name: string;
  email: string;
}

// Create a validator type where each field is a validation function
type Validator<T> = {
  [K in keyof T]: (value: T[K]) => boolean;
};

const userValidator: Validator<User> = {
  id: (v) => v > 0,
  name: (v) => v.length > 0,
  email: (v) => v.includes("@"),
};

// Create a form type where fields are wrapped in a FormField
type FormField<T> = {
  value: T;
  error: string | null;
  touched: boolean;
};

type Form<T> = {
  [K in keyof T]: FormField<T[K]>;
};

const userForm: Form<User> = {
  id: { value: 1, error: null, touched: false },
  name: { value: "Alice", error: null, touched: true },
  email: { value: "", error: "Required", touched: true },
};

// The form type is always in sync with User — add a field to
// User and the form type automatically requires it too.
17

Gardes de type & Restriction

Restriction typeof & instanceof

TypeScript restreint les types basé sur les vérifications à l'exécution. typeof restreint les primitifs (string, number, boolean, etc.) ; instanceof restreint les instances de classe. Les vérifications de véracité (if (value)) restreignent en excluant null/undefined/0/''/false. La restriction s'applique dans la branche où la condition tient. C'est comment TypeScript fait porter des informations de type aux vérifications à l'exécution, éliminant le besoin de casts explicites.

typescript
// typeof narrows primitive types
function padLeft(value: string | number, padding: string | number) {
  if (typeof padding === "number") {
    return " ".repeat(padding) + value;
    // padding is narrowed to 'number' here
  }
  return padding + value;
  // padding is narrowed to 'string' here
}

// instanceof narrows class types
class Cat { meow(): void {} }
class Dog { bark(): void {} }

function speak(animal: Cat | Dog) {
  if (animal instanceof Cat) {
    animal.meow(); // OK — narrowed to Cat
  } else {
    animal.bark(); // OK — narrowed to Dog
  }
}

// typeof returns: "string" | "number" | "boolean" | "symbol"
//   "bigint" | "undefined" | "object" | "function"
// Note: typeof null === "object" (historical JS bug)

// Truthiness narrowing
function process(value?: string) {
  if (value) {
    console.log(value.toUpperCase()); // value is string (not undefined)
  }
}

Opérateur in & Unions discriminées

L'opérateur 'in' restreint basé sur l'existence de propriété. Les unions discriminées utilisent une propriété littérale partagée (comme 'kind' ou 'type') comme étiquette — switcher dessus restreint à la bonne variante avec accès complet aux propriétés. C'est l'équivalent TypeScript des types somme / types de données algébriques. C'est le patron standard pour les actions Redux, les machines à états et les réponses d'API avec formes multiples. Utilisez toujours un type littéral pour le discriminant.

typescript
// 'in' operator checks for a property — narrows to types that have it

interface Fish { swim(): void; }
interface Bird { fly(): void; }

function move(animal: Fish | Bird) {
  if ("swim" in animal) {
    animal.swim(); // narrowed to Fish
  } else {
    animal.fly(); // narrowed to Bird
  }
}

// Discriminated unions: a shared literal property (the 'discriminant')
type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; size: number }
  | { kind: "rectangle"; width: number; height: number };

function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2; // narrowed by kind
    case "square":
      return shape.size ** 2;
    case "rectangle":
      return shape.width * shape.height;
  }
}

// Discriminated unions are the idiomatic TS pattern for
// variant data (like Redux actions, AST nodes, API responses)

Fonctions de garde de type personnalisées

Les gardes de type personnalisées (type de retour 'x is Type') vous permettent d'encapsuler des vérifications à l'exécution complexes dans des fonctions réutilisables qui restreignent les types. Les fonctions d'assertion (asserts x is T) lèvent une exception au lieu de renvoyer un booléen — elles restreignent dans tout le code après l'appel. Utilisez les gardes de type pour valider les données non fiables (JSON.parse, réponses d'API) et les amener dans le système de type. Cela fait le pont entre la validation à l'exécution et les types à la compilation.

typescript
// A type guard function has a special return type: 'x is Type'
// It narrows the type when it returns true

interface User {
  id: number;
  name: string;
}

function isUser(obj: any): obj is User {
  return (
    typeof obj === "object" &&
    obj !== null &&
    typeof obj.id === "number" &&
    typeof obj.name === "string"
  );
}

const data: unknown = JSON.parse('{"id": 1, "name": "Alice"}');

if (isUser(data)) {
  console.log(data.name); // data is narrowed to User
}

// Type guard for arrays
function isStringArray(arr: unknown): arr is string[] {
  return Array.isArray(arr) && arr.every((x) => typeof x === "string");
}

// Type guard for discriminated unions
type Result<T> =
  | { success: true; data: T }
  | { success: false; error: string };

function isSuccess<T>(r: Result<T>): r is { success: true; data: T } {
  return r.success;
}

// Assertion functions (TS 3.7+): throw if condition fails
function assertDefined<T>(value: T | null | undefined): asserts value is T {
  if (value === null || value === undefined) {
    throw new Error("Expected value to be defined");
  }
}

Vérification d'exhaustivité avec never

La vérification d'exhaustivité utilise le type 'never' pour garantir que toutes les variantes d'union sont gérées. Si vous ajoutez une nouvelle variante à l'union mais oubliez un cas, l'affectation 'never' de la branche par défaut devient une erreur de compilation. L'assistant assertNever lève une exception à l'exécution et erreure à la compilation pour les cas manquants. C'est le patron le plus valuable pour les unions discriminées — il fait dire au compilateur quand vous avez oublié un cas.

typescript
// The 'never' type ensures all cases are handled

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; size: number };

function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.size ** 2;
    default:
      // If we add a new shape but forget a case, this line
      // becomes a compile error because 'shape' would not be 'never'
      const _exhaustive: never = shape;
      return _exhaustive;
  }
}

// Adding a new variant without updating area():
// type Shape = ... | { kind: "triangle"; base: number; height: number }
// Error: Type '{ kind: "triangle"; ... }' is not assignable to type 'never'

// Helper function pattern
function assertNever(x: never): never {
  throw new Error("Unexpected: " + JSON.stringify(x));
}

function process(shape: Shape) {
  switch (shape.kind) {
    case "circle": return;
    case "square": return;
    default: return assertNever(shape); // compile error if a case is missing
  }
}

Restriction avec méthodes de tableau

Array.filter ne restreint pas les types d'élément par défaut car son rappel renvoie un booléen, pas une garde de type. Pour restreindre, passez une fonction de garde de type personnalisée (pet is Dog) — alors filter renvoie le type de tableau restreint. TypeScript restreint aussi dans les corps de rappel (forEach, map) basé sur les if-checks. Array.isArray est une garde de type intégrée qui restreint unknown/any à un type de tableau.

typescript
// TypeScript narrows through filter, map, and control flow

interface Pet {
  name: string;
  speak(): void;
}

class Dog implements Pet {
  name = "Rex";
  speak() { console.log("Woof"); }
  fetch() { console.log("Fetching!"); }
}

class Cat implements Pet {
  name = "Whiskers";
  speak() { console.log("Meow"); }
}

const pets: Pet[] = [new Dog(), new Cat(), new Dog()];

// filter doesn't narrow by default (callback return type is boolean)
// Use a type guard to narrow:
function isDog(pet: Pet): pet is Dog {
  return pet instanceof Dog;
}

const dogs = pets.filter(isDog); // Dog[] — properly narrowed!
dogs.forEach((d) => d.fetch()); // OK — d is Dog

// Narrowing in forEach/callbacks
pets.forEach((pet) => {
  if (isDog(pet)) {
    pet.fetch(); // narrowed to Dog inside callback
  }
});

// Array.isArray narrows 'unknown' to an array
function process(input: unknown) {
  if (Array.isArray(input)) {
    input.length; // OK — input is any[] (or unknown[])
  }
}
18

Inférence de type

Inférence de variable & Type de retour

TypeScript infère les types depuis les initialiseurs et les instructions de retour, donc vous avez rarement besoin d'annotations explicites. Les variables s'élargissent à leur type général (let x = 10 infère number, pas 10). 'as const' empêche l'élargissement : cela fait que les littéraux restent littéraux, les objets en lecture seule, et les tableaux deviennent des tuples en lecture seule. typeof colors[number] extrait une union des types d'éléments de tuple — un patron courant pour dériver des types de type enum depuis des tableaux.

typescript
// TypeScript infers types when you don't annotate

// Variable inference
let count = 0;          // number
let name = "Alice";     // string
let items = [1, 2, 3];  // number[]
let mixed = [1, "two"]; // (string | number)[]

// Function return type inference
function add(a: number, b: number) {
  return a + b; // return type inferred as number
}

// const assertions for literal types
const x = 10;        // number (widened)
const y = "hello";   // string (widened)
const z = { a: 1 };  // { a: number }

const x2 = 10 as const;       // 10 (literal type)
const y2 = "hello" as const;  // "hello" (literal type)
const z2 = { a: 1 } as const; // { readonly a: 10 }

// Array with 'as const' becomes a readonly tuple
const colors = ["red", "green", "blue"] as const;
// readonly ["red", "green", "blue"]
type Color = typeof colors[number]; // "red" | "green" | "blue"

Typage contextuel

Le typage contextuel fait couler le type attendu à rebours dans les expressions. Lorsque vous affectez une fonction à une variable typée, les types de paramètres sont inférés depuis le type cible. C'est pourquoi les gestionnaires d'événements, les rappels de tableau et les littéraux d'objet n'ont souvent pas besoin d'annotations de type. La règle empirique : annotez les signatures de fonction (paramètres et types de retour pour les APIs publiques), mais laissez l'inférence gérer les locales et les rappels.

typescript
// Contextual typing: the expected type influences the inferred type

// Without context, the parameter type is unknown
window.onmousedown = (mouseEvent) => {
  // mouseEvent is inferred as MouseEvent (from onmousedown's type)
  console.log(mouseEvent.button);
};

// Array map: callback params inferred from array type
const nums = [1, 2, 3];
const doubled = nums.map((n) => n * 2); // n is number, result is number[]

// Contextual typing for object literals
interface User {
  name: string;
  age: number;
}
const user: User = {
  name: "Alice", // inferred as string (from User.name)
  age: 30,
};

// Contextual typing with discriminated unions
type Event =
  | { type: "click"; x: number; y: number }
  | { type: "scroll"; scrollTop: number };

function handle(e: Event) {
  if (e.type === "click") {
    console.log(e.x, e.y); // narrowed
  }
}

// Best practice: let inference work for you — don't over-annotate
const nums2 = [1, 2, 3].map((n) => n * 2); // number[] — no annotation needed

Meilleur type commun (Inférence d'union)

Lors de l'inférence depuis plusieurs valeurs (comme les littéraux de tableau), TypeScript trouve le 'meilleur type commun' — généralement un supertype ou une union. Un tableau de [Dog, Cat] infère comme Animal[] (la base commune), pas (Dog | Cat)[]. Pour obtenir une union, annotez explicitement. Les retours conditionnels infèrent l'union de toutes les branches. Comprendre cela aide à prédire quand vous avez besoin d'annotations explicites vs quand l'inférence suffit.

typescript
// When inferring from multiple values, TS finds the best common type

// Array of same type: inferred as that type
const nums = [1, 2, 3]; // number[]

// Array of different types: inferred as union
const mixed = [1, "two", true]; // (string | number | boolean)[]

// Array of subclasses: inferred as the common supertype
class Animal { name: string; }
class Dog extends Animal { bark(): void {} }
class Cat extends Animal { meow(): void {} }

const pets = [new Dog(), new Cat()]; // Animal[] (not (Dog | Cat)[])

// To get a union instead, use an explicit type annotation:
const pets2: (Dog | Cat)[] = [new Dog(), new Cat()];

// Or use 'as const' for readonly tuples:
const tuple = [new Dog(), new Cat()] as const;
// readonly [Dog, Cat]

// Return type inference with conditionals
function getValue(flag: boolean) {
  return flag ? 42 : "hello"; // inferred as number | string
}

Analyse de flux de contrôle

TypeScript effectue une analyse de flux de contrôle — il suit comment les types se restreignent et s'élargissent à travers if/else, retours, affectations et opérateurs logiques. Un type se restreint après une vérification et reste restreint jusqu'à ce que la variable soit réaffectée. Les retours anticipés (clauses de garde) sont particulièrement efficaces : après 'if (value === null) return', le reste de la fonction sait que value n'est pas null. C'est pourquoi le code style clause-de-garde fonctionne si bien avec TypeScript.

typescript
// TypeScript tracks types through control flow

function example(value: string | number | null) {
  // After this check, 'value' is string | number in this branch
  if (value === null) {
    return;
  }

  // value is now string | number (null narrowed out)
  console.log(value);

  if (typeof value === "string") {
    return value.toUpperCase(); // value is string
  }

  // value is now number (string and null narrowed out)
  return value.toFixed(2);
}

// Narrowing through assignments
let x: string | number;
x = "hello";
console.log(x.toUpperCase()); // x is string
x = 42;
console.log(x.toFixed(2)); // x is number

// Narrowing with && and ||
function process(input?: string) {
  const value = input && input.trim(); // string | undefined
  const safe = input || "default"; // string (undefined narrowed out)
}

// Narrowing is reset when a variable is reassigned
let v: string | number = "hi";
v.toUpperCase(); // string
v = 42;
// v.toUpperCase(); // Error: number doesn't have toUpperCase

Opérateur satisfies (TS 4.9+)

L'opérateur 'satisfies' (TS 4.9+) valide qu'une valeur se conforme à un type tout en préservant le type inféré le plus spécifique — contrairement aux annotations de type qui élargissent. C'est idéal pour les configs, les maps de routes et les objets de thème : vous obtenez une validation à la compilation que la structure est correcte, mais l'accès aux propriétés renvoie toujours le type littéral précis. Combinez avec 'as const' pour à la fois la préservation littérale et la validation structurelle.

typescript
// 'satisfies' checks a value matches a type WITHOUT widening it

// Problem: annotation widens the type
const config1: Record<string, string | number> = {
  port: 8080,
  host: "localhost",
};
// config1.port is 'string | number' (widened — lost the literal)

// Solution: 'satisfies' validates but preserves the inferred type
const config2 = {
  port: 8080,
  host: "localhost",
} satisfies Record<string, string | number>;
// config2.port is 'number' (preserved!) but still validated

// Practical: type-safe route configs
type Routes = Record<string, { method: string; handler: () => void }>;

const routes = {
  "/users": { method: "GET", handler: () => fetchUsers() },
  "/posts": { method: "POST", handler: () => createPost() },
} satisfies Routes;

routes["/users"].method; // string (from Routes)
routes["/users"].handler; // () => void (from Routes)

// 'as const' + 'satisfies': literal types + validation
const colors = {
  primary: "#ff0000",
  secondary: "#00ff00",
} as const satisfies Record<string, `#${string}`;
19

Fichiers de déclaration & Augmentation de module

Écrire des fichiers de déclaration .d.ts

Les fichiers .d.ts contiennent des déclarations de type (pas d'implémentation) — ils décrivent les types du code JavaScript. Utilisez 'declare module' pour ajouter des types aux packages npm non typés. 'declare global' étend les types globaux comme Window. Les déclarations ambiantes disent à TypeScript 'cela existe à l'exécution, faites-moi confiance'. C'est comment vous intégrez le JS hérité, les APIs de navigateur et les variables injectées au build dans le système de type.

typescript
// types/my-module.d.ts — describes the types of a JS library

// Declare a module (for libraries without types)
declare module "untyped-lib" {
  export function greet(name: string): string;
  export const version: string;
  export interface Config {
    timeout: number;
    retries: number;
  }
}

// Ambient declarations for global variables
declare global {
  interface Window {
    myCustomProp: string;
    myApp: { init: () => void };
  }
}

// Now window.myCustomProp is typed
// window.myCustomProp; // string

// Declaring a global function
declare function myGlobalFn(x: number): string;

// Declaring a global namespace
declare namespace MyLib {
  function doSomething(): void;
  const version: string;
}

// Use 'declare' for things that exist at runtime but not in TS
// Common for: legacy JS, browser globals, build-time constants

Augmentation de module (Étendre des types existants)

L'augmentation de module étend les types existants d'autres modules — ajoutant des propriétés aux interfaces sans modifier la source originale. C'est comment le middleware Express (comme passport) ajoute req.user, et comment vous étendez les types de bibliothèque tierce. La syntaxe 'declare module' rouvre l'espace de type du module. Les augmentations doivent être dans un module (un fichier avec import/export) pour prendre effet globalement.

typescript
// Module augmentation: add to an existing module's types

// Extend an interface from another module
import express from "express";

declare module "express" {
  interface Request {
    user?: {
      id: number;
      name: string;
    };
    // Now req.user is typed on all Express requests
  }
}

// Augment a third-party module
declare module "axios" {
  export interface AxiosRequestConfig {
    retryCount?: number; // add a custom config option
  }
}

// Augment a global interface
declare global {
  interface Array<T> {
    last(): T | undefined; // add a custom array method
  }
}

// Implementation (in a .ts file, not .d.ts)
Array.prototype.last = function () {
  return this[this.length - 1];
};

[1, 2, 3].last(); // 3 — now typed!

// Module augmentation is how middleware adds typed properties
// to req/res objects in Express, Fastify, etc.

Directives à triple barre oblique

Les directives à triple barre oblique (///) sont des commentaires spéciaux du compilateur qui demandent à TypeScript d'inclure des fichiers ou packages de type supplémentaires. La plus courante est /// <reference types='node' /> pour inclure @types/node. Avec les options 'types' et 'lib' modernes de tsconfig.json, elles sont rarement nécessaires — préférez les paramètres basés sur la config. Elles sont principalement vues dans les fichiers .d.ts et le code hérité. Les comprendre aide lors de la lecture de fichiers de déclaration.

typescript
// Triple-slash directives are special comments processed by TS

/// <reference path="./other-types.d.ts" />
// Includes another declaration file (rarely needed with modules)

/// <reference types="node" />
// Includes types from @types/node (like process, Buffer, __dirname)

/// <reference lib="es2020" />
// Includes a built-in lib (alternative to tsconfig "lib")

// Most common use: referencing @types packages
// In a .d.ts file for a package that needs Node types:
/// <reference types="node" />

declare function readFile(path: string): Buffer; // Buffer from @types/node

// NOTE: With modern TS and tsconfig "types" and "lib" options,
// triple-slash directives are rarely needed. They're mostly
// used in .d.ts files for backward compatibility.

// Prefer tsconfig.json settings:
// {
//   "compilerOptions": {
//     "types": ["node"],      // instead of /// <reference types="node" />
//     "lib": ["es2020", "dom"] // instead of /// <reference lib="es2020" />
//   }
// }

Publier des types avec un package

Pour publier des types TypeScript avec votre package npm, définissez 'types' dans package.json pour pointer vers votre fichier .d.ts et activez 'declaration: true' dans tsconfig. Les consommateurs obtiennent automatiquement les types lorsqu'ils installent votre package. declarationMap active 'Aller à la définition' pour sauter au fichier source .ts. Pour les bibliothèques sans types intégrés, le projet DefinitelyTyped (@types/package) fournit des déclarations maintenues par la communauté.

typescript
// package.json for a library with TypeScript types
{
  "name": "my-library",
  "version": "1.0.0",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",  // <-- points to the type declarations
  "files": ["dist"],
  "scripts": {
    "build": "tsc",
    "prepublishOnly": "npm run build"
  }
}

// tsconfig.json for building a library
{
  "compilerOptions": {
    "declaration": true,        // generate .d.ts files
    "declarationMap": true,     // generate .d.ts.map (source maps for types)
    "sourceMap": true,          // generate .js.map
    "outDir": "./dist",
    "rootDir": "./src",
    "composite": true           // enable project references
  }
}

// Consumers get types automatically when they 'npm install my-library'
import { myFunction } from "my-library"; // fully typed!

// For libraries without bundled types, install @types package:
// npm install --save-dev @types/express

// Check if types exist: https://www.typescriptlang.org/dt/search

Imports & Exports de type uniquement

'import type' importe uniquement des informations de type — c'est complètement effacé à l'exécution, réduisant la taille du bundle et évitant les problèmes de dépendances circulaires. Utilisez-le pour les interfaces, alias de type et ré-exports de type uniquement. La syntaxe 'import { x, type Y }' en ligne (TS 4.5+) mélange les imports de valeur et de type proprement. verbatimModuleSyntax (TS 5.0+) applique cela strictement. Préférez 'import type' chaque fois que vous importez quelque chose utilisé uniquement dans des positions de type.

typescript
// 'import type' imports only types (erased at runtime, no JS emitted)
import type { User, Config } from "./types";
import type { ReactNode } from "react";

// These are erased completely — no runtime import in the output JS
// Useful for: reducing bundle size, avoiding circular dependencies

// Mixed import: value + type
import { createStore, type Store } from "redux";
// createStore is a runtime import; Store is type-only

// 'export type' re-exports types only
export type { User, Config } from "./types";

// Interface vs Type for declarations
interface IUser { name: string; }  // can be augmented/merged
type TUser = { name: string };     // cannot be merged, more flexible

// VerbatimModuleSyntax (TS 5.0+): enforces 'import type' for type-only
// imports, ensuring they're always elided
// {
//   "compilerOptions": { "verbatimModuleSyntax": true }
// }
20

Options tsconfig.json

Options du compilateur principal

Le tsconfig.json contrôle comment TypeScript compile. 'target' définit la version JS de sortie ; 'module' définit le système de module. 'strict: true' est le paramètre le plus important — il active toutes les vérifications de type strictes (noImplicitAny, strictNullChecks, etc.). 'lib' détermine quelles APIs intégrées sont disponibles (DOM pour le navigateur, ES2022 pour les fonctionnalités JS modernes). Commencez toujours les nouveaux projets avec strict: true.

typescript
{
  "compilerOptions": {
    "target": "ES2022",        // JS version to compile to
    "module": "ESNext",        // module system (ESNext, CommonJS, etc.)
    "moduleResolution": "bundler", // how modules are resolved
    "lib": ["ES2022", "DOM"],  // available type definitions
    "outDir": "./dist",        // output directory for compiled JS
    "rootDir": "./src",        // root of source files
    "sourceMap": true,         // generate .map files for debugging
    "declaration": true,       // generate .d.ts files (for libraries)
    "removeComments": true,    // strip comments from output

    // Type checking strictness
    "strict": true,            // enable ALL strict checks (recommended)
    "noImplicitAny": true,     // error on implicit 'any'
    "strictNullChecks": true,  // null/undefined are separate types
    "noUnusedLocals": true,    // error on unused local variables
    "noUnusedParameters": true,// error on unused function params
    "noImplicitReturns": true, // error if not all paths return
    "noFallthroughCasesInSwitch": true,

    "esModuleInterop": true,   // allow default imports from CommonJS
    "skipLibCheck": true,      // skip type checking of .d.ts files
    "forceConsistentCasingInFileNames": true
  }
}

Indicateurs du mode strict expliqués

Le mode strict est un ensemble d'indicateurs de rigueur. strictNullChecks est le plus impactant — il fait de null/undefined des types distincts, vous forçant à les gérer explicitement (la source #1 de plantages à l'exécution). noImplicitAny empêche l'érosion silencieuse du type. strictPropertyInitialization capture les champs de classe non initialisés (utilisez ! pour l'affectation définie ou initialisez dans le constructeur). Activez toujours le mode strict dans les nouveaux projets — le coût initial vaut la sécurité.

typescript
{
  "compilerOptions": {
    // 'strict: true' enables ALL of these:

    "strictNullChecks": true,
    // null and undefined are NOT assignable to other types
    // without explicit union. Forces null handling.
    let x: string = null; // ERROR (without this, it's allowed)

    "noImplicitAny": true,
    // Parameters/variables can't be implicitly 'any'
    function fn(x) { } // ERROR: x is implicitly any

    "strictFunctionTypes": true,
    // Function parameter types checked contravariantly
    // (catches unsafe function assignments)

    "strictBindCallApply": true,
    // bind/call/apply are type-checked

    "strictPropertyInitialization": true,
    // Class properties must be initialized or declared with !
    class User {
      name: string; // ERROR: not initialized
      name2!: string; // OK: definite assignment assertion
    }

    "noImplicitThis": true,
    // 'this' must have a known type (no implicit any)

    "alwaysStrict": true
    // Emit "use strict" in every file
  }
}

Stratégies de résolution de module

moduleResolution contrôle comment les chemins d'import sont résolus. 'node' est la stratégie classique ; 'bundler' (TS 5.0+) correspond aux bundlers modernes comme Vite et prend en charge les exports de package.json. 'nodenext' est ESM strict (nécessite des extensions). paths vous permet de créer des alias d'import (@/ → src/), qui doivent être reflétés dans votre config de bundler (ex. resolve.alias de Vite). baseUrl + paths est la méthode standard pour éviter les imports relatifs profonds (../../../).

typescript
{
  "compilerOptions": {
    // How TS resolves import paths

    "moduleResolution": "node",    // classic Node.js resolution
    // Looks for: file.ts, file/index.ts, node_modules/file

    "moduleResolution": "bundler", // for Vite/webpack/esbuild (TS 5.0+)
    // Matches how bundlers resolve: supports import maps,
    // conditional exports, no file extension requirement

    "moduleResolution": "nodenext", // Node.js ESM resolution (strict)
    // Requires file extensions in imports: import "./foo.js"

    // Path mapping (aliases)
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],           // import "@/components/Button"
      "@utils/*": ["src/utils/*"],
      "@components": ["src/components/index.ts"]
    }

    // rootDirs: virtual directories that map to the same location
    "rootDirs": ["src", "generated"],
    // imports between src/ and generated/ resolve as if same dir
  }
}

// In your code:
import { Button } from "@/components/Button";
import { formatDate } from "@utils/date";
// These resolve to src/components/Button.ts and src/utils/date.ts

Références de projet (Monorepos)

Les références de projet divisent une grande codebase en sous-projets compilés indépendamment — essentielles pour les monorepos. Chaque projet a composite: true et émet des déclarations. Les références déclarent des dépendances entre projets. tsc --build (-b) compile dans l'ordre des dépendances, ne recompilant que ce qui a changé. Cela accélère considérablement la vérification de type pour les grandes codebases et applique des frontières architecturales entre les packages.

typescript
// tsconfig.json (root — references sub-projects)
{
  "files": [],
  "references": [
    { "path": "./packages/shared" },
    { "path": "./packages/api" },
    { "path": "./packages/web" }
  ]
}

// packages/shared/tsconfig.json
{
  "compilerOptions": {
    "composite": true,          // required for project references
    "declaration": true,        // must emit declarations
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src"]
}

// packages/api/tsconfig.json
{
  "compilerOptions": { /* ... */ },
  "references": [
    { "path": "../shared" }     // depends on shared
  ]
}

// Benefits:
// 1. Faster builds — only rebuild changed projects
// 2. Clear dependency boundaries between packages
// 3. Type-checking is scoped per project
// 4. tsc --build handles the build order automatically

// Build command: tsc --build (or tsc -b)

Recettes tsconfig courantes

Différents types de projets nécessitent différents paramètres tsconfig. React/Vite utilise jsx: 'react-jsx' et noEmit (Vite compile). Node.js utilise CommonJS (ou NodeNext pour ESM) et types: ['node']. Les bibliothèques nécessitent declaration: true pour la sortie .d.ts et une cible inférieure pour une compatibilité plus large. isolatedModules est requis par Vite/esbuild (chaque fichier doit être compilable indépendamment). Excluez toujours les fichiers de test et node_modules du build.

typescript
// React + Vite project
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "jsx": "react-jsx",          // React 17+ JSX transform
    "lib": ["ES2022", "DOM", "DOM.Iterable"],
    "strict": true,
    "noEmit": true,             // Vite handles compilation
    "isolatedModules": true,    // required by Vite/esbuild
    "verbatimModuleSyntax": true
  },
  "include": ["src"]
}

// Node.js backend project
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "CommonJS",        // or NodeNext for ESM
    "moduleResolution": "node",
    "lib": ["ES2022"],
    "types": ["node"],           // @types/node
    "outDir": "./dist",
    "strict": true,
    "esModuleInterop": true
  },
  "include": ["src"]
}

// Library project (publishes to npm)
{
  "compilerOptions": {
    "target": "ES2020",          // broader compatibility
    "module": "ESNext",
    "declaration": true,         // emit .d.ts
    "declarationMap": true,
    "outDir": "./dist",
    "strict": true
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist", "**/*.test.ts"]
}
21

Décorateurs

Décorateur de classe

Les décorateurs de classe reçoivent le constructeur et peuvent renvoyer une classe modifiée. Ils sont expérimentaux (nécessitent experimentalDecorators: true). Courants dans NestJS et TypeORM.

typescript
function logged<T extends new (...args: any[]) => any>(con: T) {
    return class extends con {
        created = new Date().toISOString();
    };
}
@logged
class Service { constructor(public name: string) {} }

Décorateur de méthode

Les décorateurs de méthode reçoivent (target, key, descriptor). Envelopper descriptor.value active la journalisation, le cache, la validation. C'est comment les intercepteurs NestJS fonctionnent.

typescript
function log(target: any, key: string, desc: PropertyDescriptor) {
    const orig = desc.value;
    desc.value = function(...args: any[]) {
        console.log(`Calling ${key}`, args);
        return orig.apply(this, args);
    };
}
class Calc { @log add(a: number, b: number) { return a + b; } }

Décorateur de propriété

Les décorateurs de propriété reçoivent (target, key). Utiliser Object.defineProperty crée des getters/setters pour la validation. Utilisé dans class-validator pour la validation de DTO.

typescript
function required(target: any, key: string) {
    let val: any;
    Object.defineProperty(target, key, {
        get() { return val; },
        set(v: any) { if (v == null) throw new Error(`${key} required`); val = v; }
    });
}

Décorateur de paramètre

Les décorateurs de paramètre reçoivent (target, methodKey, parameterIndex). Utilisés avec la réflexion de métadonnées pour la validation. class-validator et NestJS utilisent cela.

typescript
function Min(min: number) {
    return (target: any, key: string, idx: number) => {
        console.log(`${key} param ${idx} >= ${min}`);
    };
}
class Order { create(@Min(0) qty: number) { return qty; } }

Fabrique de décorateurs

Les fabriques de décorateurs renvoient une fonction décorateur, permettant la configuration. La fonction externe reçoit les paramètres, l'interne est le véritable décorateur.

typescript
function Retry(times: number) {
    return function(target: any, key: string, desc: PropertyDescriptor) {
        const orig = desc.value;
        desc.value = async function(...args: any[]) {
            for (let i = 0; i < times; i++)
                try { return await orig.apply(this, args); }
                catch (e) { if (i === times - 1) throw e; }
        };
    };
}
22

Augmentation de module

Augmenter les types intégrés

L'augmentation de module étend les types existants. declare global permet d'augmenter les types intégrés comme Array. L'implémentation à l'exécution doit également être fournie.

typescript
declare global {
    interface Array<T> {
        last(): T | undefined;
        chunk(size: number): T[][];
    }
}
Array.prototype.last = function() { return this[this.length - 1]; };

Augmenter les types de bibliothèque

L'augmentation de module étend les types des bibliothèques tierces. declare module rouvre le type du module. Essentiel pour ajouter des propriétés personnalisées aux objets de framework.

typescript
declare module 'express' {
    interface Request {
        user?: { id: string; role: string };
    }
}
app.get('/profile', (req, res) => {
    const userId = req.user?.id;  // Typed!
});

Augmenter Window

Augmenter Window ajoute des propriétés globales personnalisées avec une sécurité de type. Utile pour exposer l'état de l'application aux outils de débogage ou d'analyse.

typescript
declare global {
    interface Window {
        myApp: { init: () => void; version: string };
    }
}
window.myApp = { init: () => console.log('Ready'), version: '1.0.0' };

CSS Modules

Les CSS Modules nécessitent des déclarations de type. La déclaration associe les imports .module.css à un enregistrement de noms de classes. Active l'autocomplétion pour les références de classes CSS.

typescript
declare module '*.module.css' {
    const classes: { readonly [key: string]: string };
    export default classes;
}
import styles from './Button.module.css';
<button className={styles.button} />

Plugin Vue

Vue et d'autres frameworks utilisent l'augmentation de module pour le typage des plugins. ComponentCustomProperties ajoute des propriétés d'instance. Active les plugins typés de manière sécurisée.

typescript
declare module 'vue' {
    interface ComponentCustomProperties {
        $auth: { login: () => Promise<void> };
    }
}
export default defineComponent({
    methods: { async login() { await this.$auth.login(); } }
});
23

Fusion de déclarations

Fusionner les interfaces

Les interfaces portant le même nom sont automatiquement fusionnées. Tous les membres font partie d'une seule interface. Utile pour répartir les interfaces entre plusieurs fichiers.

typescript
interface User { name: string; }
interface User { age: number; }
interface User { email: string; }
const user: User = { name: 'Alice', age: 30, email: '[email protected]' };

Fusionner les espaces de noms

Les espaces de noms portant le même nom fusionnent leurs exports. Cela permet de répartir le contenu d'un espace de noms entre plusieurs fichiers. Les modules ES sont préférés pour le nouveau code.

typescript
namespace Utils {
    export function log(msg: string) { console.log(msg); }
}
namespace Utils {
    export function warn(msg: string) { console.warn(msg); }
}
Utils.log('info'); Utils.warn('alert');

Espace de noms avec fonction

Les espaces de noms peuvent fusionner avec des fonctions, des classes et des enums. L'espace de noms ajoute des propriétés statiques à la fonction. Utilisé dans Moment.js et des bibliothèques similaires.

typescript
function Counter() { Counter.count++; }
namespace Counter {
    export let count = 0;
    export function reset() { count = 0; }
}
Counter(); Counter();
console.log(Counter.count);  // 2

Fusion avec les classes

Fusionner un espace de noms avec une classe ajoute des membres statiques et des types imbriqués. L'espace de noms peut exporter des interfaces qui deviennent des types imbriqués.

typescript
class Settings { static defaults = { theme: 'light' }; }
namespace Settings {
    export interface Options { theme: string; lang: string; }
}
const opts: Settings.Options = { theme: 'dark', lang: 'en' };

Fusions interdites

Les classes ne peuvent pas fusionner avec d'autres classes. Les variables ne peuvent pas fusionner. Les fonctions fusionnent en tant que surcharges. Les enums peuvent fusionner avec des espaces de noms.

typescript
// Cannot merge classes
class A { x = 1; }
class A { y = 2; }  // Error
// Function overloads (allowed)
function fn(x: string): string;
function fn(x: number): number;
function fn(x: any): any { return x; }
24

Rétrécissement de type

typeof & instanceof

typeof rétrécit les types primitifs. instanceof rétrécit les types de classe. TypeScript comprend ces vérifications et rétrécit le type dans chaque branche.

typescript
function process(value: string | number | Date) {
    if (typeof value === 'string') return value.toUpperCase();
    if (typeof value === 'number') return value.toFixed(2);
    if (value instanceof Date) return value.toISOString();
}

Opérateur in

L'opérateur in vérifie si une propriété existe, rétrécissant le type. Utile pour les unions discriminées avec différents noms de propriétés.

typescript
interface Cat { meow(): void; }
interface Dog { bark(): void; }
function speak(animal: Cat | Dog) {
    if ('meow' in animal) animal.meow();
    else animal.bark();
}

Unions discriminées

Les unions discriminées utilisent une propriété commune (discriminant) pour rétrécir les types. switch sur le discriminant pour une vérification exhaustive. Le modèle le plus sûr pour les types variants.

typescript
type Result =
    | { status: 'success'; data: string }
    | { status: 'error'; message: string };
function handle(r: Result) {
    switch (r.status) {
        case 'success': console.log(r.data); break;
        case 'error': console.log(r.message); break;
    }
}

Prédicats de type

Les prédicats de type (x is T) permettent des fonctions de rétrécissement personnalisées. Retourner true rétrécit vers T, false rétrécit vers le type exclu. TypeScript fait confiance au prédicat aveuglément.

typescript
function isFish(pet: Fish | Bird): pet is Fish {
    return (pet as Fish).swim !== undefined;
}
function move(pet: Fish | Bird) {
    if (isFish(pet)) pet.swim();
    else pet.fly();
}

Fonctions d'assertion

Les fonctions d'assertion lèvent une exception si la condition échoue, rétrécissant le type pour le code subséquent. asserts x is T rétrécit vers T. Élimine les vérifications de nullité redondantes.

typescript
function assertDefined<T>(value: T | null | undefined): asserts value is T {
    if (value == null) throw new Error('Null or undefined');
}
function processUser(user?: User) {
    assertDefined(user);
    console.log(user.name);  // User (not undefined)
}
25

Types littéraux de gabarit

Gabarits littéraux de base

Les types littéraux de gabarit créent des motifs de chaîne. Ils contraignent les chaînes à correspondre à un gabarit. Active des motifs typés de manière sécurisée pour les points de terminaison API et les noms d'événements.

typescript
type Greeting = `hello ${string}`;
const g: Greeting = 'hello world';  // OK
type Endpoint = `${'GET' | 'POST'} /api/${string}`;
const ep: Endpoint = 'GET /api/users';

Uppercase & Lowercase

Les types intrinsèques intégrés transforment les types littéraux de chaîne. Combiner avec les gabarits littéraux pour générer des noms d'événements et des constantes typés de manière sécurisée.

typescript
type Upper = Uppercase<'hello'>;  // 'HELLO'
type Lower = Lowercase<'WORLD'>;  // 'world'
type Cap = Capitalize<'foo'>;     // 'Foo'
type EventName = `on${Capitalize<'click'>}`;  // 'onClick'

Remappage de clés

Le remappage de clés (clause as) transforme les clés lors des types mappés. Génère des noms getter/setter à partir des noms de propriétés. Crée des API typées de manière sécurisée à partir d'interfaces.

typescript
type Getters<T> = {
    [K in keyof T as `get${Capitalize<string & K>}>`]: () => T[K];
};
interface Person { name: string; age: number; }
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number; }

Correspondance de motifs de chaîne

Les types littéraux de gabarit avec infer peuvent analyser des chaînes au moment de la compilation. Split décompose une chaîne en un tuple. Active une manipulation de chaîne typée de manière sécurisée.

typescript
type Split<S extends string, D extends string> =
    S extends `${infer L}${D}${infer R}` ? [L, ...Split<R, D>] : [S];
type Parts = Split<'a,b,c', ','>;  // ['a', 'b', 'c']

Typage de système d'événements

Les types littéraux de gabarit avec les génériques créent des systèmes d'événements entièrement typés de manière sécurisée. Le nom de l'événement détermine le type de charge utile. on et emit appliquent les types correspondants.

typescript
type EventMap = { click: { x: number }; submit: { value: string } };
class Emitter {
    on<K extends keyof EventMap>(event: K, handler: (e: EventMap[K]) => void) {}
    emit<K extends keyof EventMap>(event: K, data: EventMap[K]) {}
}
em.on('click', e => console.log(e.x));  // Typed!
26

Mot-clé infer

Extraire le type de retour

infer déclare une variable de type dans un type conditionnel. Il capture le type à une position spécifique. ReturnType est l'équivalent intégré.

typescript
type ReturnOf<T> = T extends (...args: any[]) => infer R ? R : never;
function getUser() { return { name: 'Alice', age: 30 }; }
type User = ReturnOf<typeof getUser>;  // { name: string; age: number; }

Extraire le type Promise

infer extrait le type interne d'une Promise. DeepUnwrap déroule récursivement les Promises imbriquées. Le Awaited<T> intégré fait cela dans le TypeScript moderne.

typescript
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type R = UnwrapPromise<Promise<string>>;  // string
type DeepUnwrap<T> = T extends Promise<infer U> ? DeepUnwrap<U> : T;
type D = DeepUnwrap<Promise<Promise<boolean>>>;  // boolean

Extraire l'élément de tableau

infer E capture le type d'élément d'un tableau. Pour les tuples, infer peut capturer des positions spécifiques. Utile pour travailler avec des collections génériques.

typescript
type ElementOf<T> = T extends (infer E)[] ? E : never;
type T1 = ElementOf<string[]>;  // string
type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never;
type F = First<[string, number]>;  // string

Extraire les paramètres de fonction

infer P capture le tuple de paramètres d'une fonction. Parameters est l'équivalent intégré. Utile pour envelopper des fonctions tout en préservant les types.

typescript
type Params<T> = T extends (...args: infer P) => any ? P : never;
function greet(name: string, age: number) { return ''; }
type P = Params<typeof greet>;  // [string, number]

Multiple infer

Plusieurs variables infer peuvent capturer différentes parties d'un type simultanément. Active des transformations de type complexes dans un seul conditionnel.

typescript
type FirstLast<T extends any[]> =
    T extends [infer First, ...any[], infer Last]
        ? { first: First; last: Last } : never;
type R = FirstLast<[1, 2, 3, 4]>;  // { first: 1; last: 4 }
27

Variance

Covariance

La covariance permet à Dog[] d'être assigné à Animal[]. Les tableaux TypeScript sont covariants mais cela est non sûr : pousser un Animal dans un Dog[] corrompt le tableau.

typescript
class Animal { name: string; }
class Dog extends Animal { breed: string; }
let dogs: Dog[] = [new Dog()];
let animals: Animal[] = dogs;  // OK (covariant)
// animals.push(new Animal());  // Unsafe at runtime

Contravariance

La contravariance signifie qu'une fonction acceptant un Dog peut être utilisée là où une fonction acceptant un Animal est attendue. Sûr car un gestionnaire de Dog gère tout Animal qui est un Dog.

typescript
type Handler<T> = (arg: T) => void;
let dogHandler: Handler<Dog> = (d) => console.log(d.breed);
let animalHandler: Handler<Animal> = dogHandler;  // OK with strictFunctionTypes

Bivariance

La syntaxe de méthode est bivariante. La syntaxe de propriété de fonction est contravariante avec strictFunctionTypes. Les méthodes sont bivariantes pour la compatibilité OO.

typescript
interface IFace {
    method(x: Animal): void;  // bivariant
    fn: (x: Animal) => void;  // contravariant
}
class Impl implements IFace {
    method(x: Dog) {}  // OK (bivariant)
    fn = (x: Dog) => {}  // Error (contravariant)
}

Variance in/out

TypeScript 4.7+ prend en charge les annotations de variance explicites. in marque contravariant (consommateurs), out marque covariant (producteurs), in out marque invariant (les deux).

typescript
interface Producer<out T> { produce(): T; }
interface Consumer<in T> { consume(value: T): void; }
interface Buffer<in out T> { read(): T; write(value: T): void; }

Types invariants

Les types invariants nécessitent des correspondances de type exactes. Un type est invariant lorsqu'il apparaît dans les positions d'entrée et de sortie. Box<Dog> ne peut pas être assigné à Box<Animal>.

typescript
interface Box<T> { get(): T; set(value: T): void; }
let dogBox: Box<Dog> = {} as any;
let animalBox: Box<Animal> = dogBox;  // Error: invariant
28

Pattern Builder

Builder fluide

Le pattern builder construit des objets complexes étape par étape. Chaque méthode renvoie this pour le chaînage. Utile pour les requêtes SQL, les requêtes HTTP et la configuration.

typescript
class QueryBuilder {
    private parts: string[] = [];
    select(cols: string): this { this.parts.push(`SELECT ${cols}`); return this; }
    from(table: string): this { this.parts.push(`FROM ${table}`); return this; }
    where(cond: string): this { this.parts.push(`WHERE ${cond}`); return this; }
    build(): string { return this.parts.join(' '); }
}

Builder typé de manière sécurisée

Les builders typés de manière sécurisée utilisent des types conditionnels pour appliquer les champs obligatoires. build() ne renvoie Person que lorsque hasName est true. Détecte les champs manquants au moment de la compilation.

typescript
interface State { hasName: boolean; }
class Builder<S extends State> {
    name(n: string): Builder<S & { hasName: true }> { return this as any; }
    build(): S extends { hasName: true } ? Person : never { return {} as any; }
}

Builder immuable

Les builders immuables créent une nouvelle instance pour chaque modification. Le système de type suit toutes les clés ajoutées via des types d'intersection. Chaque set renvoie un nouveau type de builder.

typescript
class ImmutableBuilder<T extends object> {
    constructor(private data: T) {}
    set<K extends string, V>(key: K, value: V):
        ImmutableBuilder<T & { [P in K]: V }> {
        return new ImmutableBuilder({ ...this.data, [key]: value });
    }
    build(): T { return this.data; }
}

Pattern Director

Le Director encapsule les séquences de construction courantes. Il utilise un builder pour créer des produits standards. Différents directors produisent différentes variations.

typescript
class HTMLBuilder {
    private html = '';
    addTag(tag: string, content: string): this {
        this.html += `<${tag}>${content}</${tag}>`; return this;
    }
    build(): string { return this.html; }
}
class Director {
    buildPage(title: string, body: string): string {
        return new HTMLBuilder().addTag('title', title).addTag('body', body).build();
    }
}

Builder à étapes

Le builder à étapes applique un ordre spécifique d'appels de méthode via le système de type. Chaque étape renvoie un type différent avec seulement la méthode suivante disponible.

typescript
class Step1 { name(n: string): Step2 { return new Step2(n); } }
class Step2 {
    constructor(private name: string) {}
    age(a: number): Step3 { return new Step3(this.name, a); }
}
class Step3 {
    constructor(private name: string, private age: number) {}
    build() { return { name: this.name, age: this.age }; }
}
29

Tests avec TypeScript

Jest avec TypeScript

Utilisez ts-jest ou @swc/jest pour les tests TypeScript. describe regroupe les tests liés, it définit les cas de test. expect crée des assertions avec des matchers comme toBe, toEqual.

typescript
import { sum } from './sum';
describe('sum', () => {
    it('adds two numbers', () => {
        expect(sum(1, 2)).toBe(3);
    });
    it('handles negatives', () => {
        expect(sum(-1, -2)).toBe(-3);
    });
});

Test de types

expectTypeOf teste les types au moment de la compilation. Vérifie les types de retour, les types de paramètres et les types de promesse résolus. Fait échouer la compilation si les types sont incorrects.

typescript
import { expectTypeOf } from 'vitest';
type GetUser = (id: string) => Promise<{ name: string }>;
test('return type is correct', () => {
    const fn = {} as unknown as GetUser;
    expectTypeOf(fn).returns.toEqualTypeOf<{ name: string }>();
    expectTypeOf(fn).parameters.toEqualTypeOf<[string]>();
});

Mocking avec les types

jest.Mocked<T> crée un mock typé à partir d'une interface. jest.fn() crée des fonctions mock avec des valeurs de retour typées. Le mock est entièrement typé.

typescript
interface Database { find(id: string): Promise<User>; }
const mockDb: jest.Mocked<Database> = {
    find: jest.fn().mockResolvedValue({ id: '1', name: 'Alice' }),
};
mockDb.find.mockResolvedValueOnce({ id: '2', name: 'Bob' });

Configuration et nettoyage des tests

beforeAll s'exécute une fois avant tous les tests, afterAll une fois après. beforeEach s'exécute avant chaque test, afterEach après chacun. Utilisé pour la configuration et le nettoyage.

typescript
describe('Database', () => {
    beforeAll(async () => { db = createDatabase(); await db.connect(); });
    afterAll(async () => { await db.disconnect(); });
    beforeEach(async () => { await db.clear(); });
    afterEach(() => { jest.restoreAllMocks(); });
});

Tests basés sur les propriétés

Les tests basés sur les propriétés génèrent des entrées aléatoires pour tester les invariants. fc.assert exécute la propriété plusieurs fois. Détecte les cas limites que les tests basés sur des exemples manquent.

typescript
import { fc } from 'fast-check';
describe('sort', () => {
    it('preserves length', () => {
        fc.assert(fc.property(fc.array(fc.integer()), (arr) => {
            return [...arr].sort().length === arr.length;
        }));
    });
});
30

Pièges courants

any vs unknown

any désactive la vérification de type, masquant les bugs. unknown est typé de manière sécurisée : vous devez le rétrécir avant utilisation. Utilisez unknown pour les sources non fiables (API, JSON.parse).

typescript
// BAD: any disables type checking
function bad(data: any) { return data.foo.bar; }
// GOOD: unknown forces type checking
function good(data: unknown) {
    if (typeof data === 'object' && data !== null && 'foo' in data)
        return (data as any).foo;
}

Vérifications des propriétés excédentaires

TypeScript ne vérifie les propriétés excédentaires que sur les littéraux d'objet assignés directement. Via une variable, la vérification est ignorée. Utilisez zod pour une validation à l'exécution plus stricte.

typescript
interface User { name: string; age: number; }
// Direct literal: checked
const u1: User = { name: 'A', age: 30, extra: true };  // Error
// Via variable: not checked
const data = { name: 'A', age: 30, extra: true };
const u2: User = data;  // OK

Enum vs Union

Les enums créent des objets à l'exécution avec mappage inverse. Les types d'union sont à zéro coût à l'exécution et élagables. Préférez les types d'union pour le nouveau code.

typescript
// Enum: runtime object
enum Color { Red, Green, Blue }
// Union: no runtime
type Color2 = 'red' | 'green' | 'blue';
// Const enum: erased
const enum Dir { Up, Down }

Typage structurel

TypeScript utilise le typage structurel : les types sont compatibles si les formes correspondent. Admin est assignable à User. Cela peut causer des bugs logiques. Les types brandés ajoutent une distinction nominale.

typescript
interface User { name: string; age: number; }
interface Admin { name: string; age: number; role: string; }
const admin: Admin = { name: 'Bob', age: 40, role: 'admin' };
const user: User = admin;  // OK (structural)

Dangers des assertions de type

Les assertions de type (as) remplacent TypeScript sans vérifications à l'exécution. Utilisez la validation à l'exécution (zod, io-ts) pour les données externes. safeParse renvoie un résultat sans lever d'exception.

typescript
// BAD: assertion hides errors
const user = JSON.parse(input) as User;
// GOOD: runtime validation
import { z } from 'zod';
const schema = z.object({ name: z.string() });
const result = schema.safeParse(JSON.parse(input));
if (result.success) { const user: User = result.data; }

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.