Fonctions#

Hide code cell source

import matplotlib.pyplot as plt
import matplotlib.patches as patches
import numpy as np
import seaborn as sns

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

Les fonctions sont les briques élémentaires de tout programme. En TypeScript, elles bénéficient d’un typage fin qui va bien au-delà de la simple annotation des paramètres : surcharges, génériques, paramètres fictifs this, prédicats de types — autant de mécanismes qui permettent d’exprimer des comportements complexes tout en conservant une vérification stricte à la compilation. Ce chapitre passe en revue l’ensemble de ces fonctionnalités.

Typage des fonctions#

Annotation des paramètres et du type de retour#

La syntaxe de base consiste à annoter chaque paramètre après son nom (séparé par :) et le type de retour après la liste des paramètres :

function additionner(a: number, b: number): number {
  return a + b;
}

// Fonction fléchée
const multiplier = (a: number, b: number): number => a * b;

// Méthode dans un objet littéral
const calculatrice = {
  diviser(a: number, b: number): number {
    if (b === 0) throw new Error("Division par zéro");
    return a / b;
  },
};

TypeScript vérifie que chaque appel de fonction respecte le nombre et les types des paramètres déclarés. Un appel avec trop peu d’arguments, trop d’arguments ou des arguments du mauvais type produit une erreur à la compilation.

Inférence du type de retour#

Le type de retour peut être omis lorsque TypeScript peut l’inférer à partir des instructions return :

// TypeScript infère le retour : number
function carré(n: number) {
  return n * n;
}

// TypeScript infère le retour : string | number
function décrire(valeur: number) {
  if (valeur > 100) {
    return "grand nombre";
  }
  return valeur;
}

Annoter explicitement le type de retour des fonctions publiques et exportées est une bonne pratique : cela documente l’intention, protège contre des changements accidentels et améliore les messages d’erreur (l’erreur est signalée à la déclaration de la fonction plutôt qu’à son site d’appel).

Le type d’une fonction comme valeur#

En TypeScript, les fonctions sont des valeurs de première classe. On peut typer une variable qui contiendra une fonction à l’aide d’un type de fonction :

// Type de fonction avec la syntaxe fléchée
type OpérationBinaire = (a: number, b: number) => number;

const addition: OpérationBinaire = (a, b) => a + b;
const soustraction: OpérationBinaire = (a, b) => a - b;

// Paramètre de type fonction (callback)
function appliquer(
  valeurs: number[],
  opération: (x: number) => number
): number[] {
  return valeurs.map(opération);
}

const résultat = appliquer([1, 2, 3, 4], x => x ** 2);
// résultat : [1, 4, 9, 16]

Définition 4 (Type de fonction)

Un type de fonction décrit la signature d’une fonction : les types de ses paramètres et son type de retour. La syntaxe est (param1: Type1, param2: Type2) => TypeRetour. On peut aussi utiliser des interfaces avec une signature d’appel (call signature) pour des types de fonctions plus complexes ou qui ont des propriétés additionnelles : interface MaFonction { (x: number): string; description: string; }.

Paramètres optionnels et valeurs par défaut#

Paramètres optionnels#

Un paramètre suivi de ? est optionnel : il peut être omis lors de l’appel. Son type devient automatiquement T | undefined à l’intérieur de la fonction :

function saluer(prenom: string, titre?: string): string {
  if (titre !== undefined) {
    return `Bonjour, ${titre} ${prenom} !`;
  }
  return `Bonjour, ${prenom} !`;
}

saluer("Alice");            // OK : "Bonjour, Alice !"
saluer("Alice", "Docteure"); // OK : "Bonjour, Docteure Alice !"

Remarque 6

Les paramètres optionnels doivent toujours apparaître après les paramètres obligatoires. TypeScript signalera une erreur si un paramètre optionnel précède un paramètre obligatoire. Cette règle est cohérente avec la sémantique de JavaScript : on ne peut pas omettre un argument au milieu d’une liste de paramètres.

Valeurs par défaut#

TypeScript supporte les valeurs par défaut de la même façon que JavaScript ES6+. Un paramètre avec une valeur par défaut est implicitement optionnel à l’appel, et son type est inféré depuis la valeur par défaut :

function créerUtilisateur(
  nom: string,
  rôle: string = 'lecteur',
  actif: boolean = true
): { nom: string; rôle: string; actif: boolean } {
  return { nom, rôle, actif };
}

créerUtilisateur("Bob");                    // { nom: "Bob", rôle: "lecteur", actif: true }
créerUtilisateur("Alice", "administrateur"); // { nom: "Alice", rôle: "administrateur", actif: true }
créerUtilisateur("Clara", "éditeur", false); // { nom: "Clara", rôle: "éditeur", actif: false }

Contrairement aux paramètres marqués ?, les paramètres avec valeur par défaut ne sont pas undefined à l’intérieur de la fonction : la valeur par défaut est utilisée si l’argument est omis ou si undefined est passé explicitement.

Paramètres rest et spread#

Paramètres rest#

Un paramètre rest capture un nombre variable d’arguments dans un tableau. Il doit être le dernier paramètre de la fonction et est annoté avec ... :

function somme(...nombres: number[]): number {
  return nombres.reduce((acc, n) => acc + n, 0);
}

somme(1, 2, 3);       // 6
somme(10, 20, 30, 40); // 100
somme();               // 0

Interaction avec les tuples#

Les paramètres rest peuvent être typés avec des tuples, ce qui permet d’exprimer des listes d’arguments hétérogènes typées précisément :

function premierEtReste<T>(premier: T, ...reste: T[]): [T, T[]] {
  return [premier, reste];
}

// Spread d'un tableau comme arguments
const arguments = [1, 2, 3, 4] as const;
somme(...arguments); // OK : chaque élément est un number

L’opérateur spread à l’appel#

Symétriquement, on peut utiliser l’opérateur ... pour étaler un tableau comme arguments d’un appel de fonction. TypeScript vérifie que le tableau a le bon type et la bonne longueur :

const coordonnées: [number, number] = [10, 20];
const point = créerPoint(...coordonnées); // OK : équivalent à créerPoint(10, 20)

Surcharge de fonctions#

La surcharge de fonctions permet de déclarer plusieurs signatures pour une même fonction, selon les types des arguments. TypeScript choisira la signature correcte en fonction des arguments fournis à l’appel.

Définition 5 (Surcharge de fonction)

Une surcharge de fonction en TypeScript consiste à déclarer deux ou plusieurs signatures de surcharge (sans corps) suivies d’une unique signature d’implémentation (avec corps). Les signatures de surcharge décrivent les formes valides de l’appel. La signature d’implémentation, plus large, couvre tous les cas ; elle n’est pas visible depuis l’extérieur et ne peut pas être appelée directement.

// Signatures de surcharge (sans corps)
function chercher(id: number): Utilisateur | undefined;
function chercher(email: string): Utilisateur | undefined;
function chercher(critères: { nom: string; ville: string }): Utilisateur[];

// Signature d'implémentation (avec corps, plus large)
function chercher(
  critère: number | string | { nom: string; ville: string }
): Utilisateur | Utilisateur[] | undefined {
  if (typeof critère === 'number') {
    return baseDeDonnées.trouverParId(critère);
  } else if (typeof critère === 'string') {
    return baseDeDonnées.trouverParEmail(critère);
  } else {
    return baseDeDonnées.rechercher(critère);
  }
}

// À l'appel, TypeScript utilise les signatures de surcharge
const u1 = chercher(42);                           // Utilisateur | undefined
const u2 = chercher("alice@exemple.fr");           // Utilisateur | undefined
const u3 = chercher({ nom: "Alice", ville: "Paris" }); // Utilisateur[]

Remarque 7

La signature d’implémentation est invisible pour les appelants : elle n’apparaît pas dans l’autocomplétion. Seules les signatures de surcharge sont exposées. C’est pourquoi la signature d’implémentation doit être compatible avec toutes les surcharges — elle doit accepter tous les types d’arguments que les surcharges déclarent. Par ailleurs, il faut au moins deux signatures de surcharge pour que la surcharge soit valide ; une seule surcharge suivie d’une implémentation est inutile et TypeScript le signale.

Visualisation : résolution de surcharge#

Hide code cell source

fig, ax = plt.subplots(figsize=(15, 9))
ax.set_xlim(-0.5, 15.5)
ax.set_ylim(-0.5, 9.5)
ax.axis('off')
ax.set_title("Résolution de surcharge TypeScript : sélection de la signature selon les arguments",
             fontsize=13, fontweight='bold', pad=16)

c_call  = '#2c3e50'
c_match = '#27ae60'
c_impl  = '#e67e22'
c_err   = '#e74c3c'
c_arrow = '#7f8c8d'

# --- Appels en entrée (colonne gauche) ---
appels = [
    (1.5, 7.8, "chercher(42)",                          "number",                       c_match),
    (1.5, 5.8, 'chercher("alice@exemple.fr")',           "string",                       c_match),
    (1.5, 3.8, 'chercher({ nom: "A", ville: "P" })',    "{ nom: string; ville: string }", c_match),
    (1.5, 1.8, 'chercher(true)',                         "boolean",                      c_err),
]

for cx, cy, label, type_label, color in appels:
    rect = patches.FancyBboxPatch(
        (cx - 2.2, cy - 0.38), 4.4, 0.76,
        boxstyle="round,pad=0.08", linewidth=2,
        edgecolor=color, facecolor=color, alpha=0.15
    )
    ax.add_patch(rect)
    ax.text(cx, cy + 0.05, label, ha='center', va='center',
            fontsize=8.5, fontweight='bold', color=color,
            fontfamily='monospace')
    ax.text(cx, cy - 0.25, f"({type_label})", ha='center', va='center',
            fontsize=7.5, color=color, style='italic')

# --- Signatures de surcharge (colonne centrale) ---
ax.text(8.0, 9.2, "Signatures de surcharge", ha='center', va='center',
        fontsize=11, fontweight='bold', color=c_call)

surcharges = [
    (8.0, 7.8, "chercher(id: number): Utilisateur | undefined",      '#3498db'),
    (8.0, 6.5, 'chercher(email: string): Utilisateur | undefined',   '#9b59b6'),
    (8.0, 5.2, 'chercher(critères: {...}): Utilisateur[]',           '#16a085'),
]

for cx, cy, label, color in surcharges:
    rect = patches.FancyBboxPatch(
        (cx - 3.5, cy - 0.38), 7.0, 0.76,
        boxstyle="round,pad=0.08", linewidth=2,
        edgecolor=color, facecolor=color, alpha=0.12
    )
    ax.add_patch(rect)
    ax.text(cx, cy, label, ha='center', va='center',
            fontsize=8, color=color,
            fontfamily='monospace')

# --- Signature d'implémentation (en bas, colonne centrale) ---
impl_y = 3.4
impl_rect = patches.FancyBboxPatch(
    (4.0, impl_y - 0.6), 8.0, 1.2,
    boxstyle="round,pad=0.1", linewidth=2.5,
    edgecolor=c_impl, facecolor=c_impl, alpha=0.12
)
ax.add_patch(impl_rect)
ax.text(8.0, impl_y + 0.18,
        "Signature d'implémentation (interne)",
        ha='center', va='center',
        fontsize=9, fontweight='bold', color=c_impl)
ax.text(8.0, impl_y - 0.2,
        "chercher(critère: number | string | {...}): ...",
        ha='center', va='center',
        fontsize=8, color=c_impl, fontfamily='monospace')

# Cas d'erreur
err_rect = patches.FancyBboxPatch(
    (4.5, 1.1), 7.0, 0.76,
    boxstyle="round,pad=0.08", linewidth=2,
    edgecolor=c_err, facecolor=c_err, alpha=0.12
)
ax.add_patch(err_rect)
ax.text(8.0, 1.48, "✗  Aucune surcharge ne correspond → erreur de compilation",
        ha='center', va='center',
        fontsize=9, color=c_err, fontweight='bold')

# --- Flèches de résolution ---
# appel 1 → surcharge 1
for (acx, acy, _, _, acolor), (scx, scy, _, scolor) in [
    (appels[0], surcharges[0]),
    (appels[1], surcharges[1]),
    (appels[2], surcharges[2]),
]:
    ax.annotate('',
        xy=(scx - 3.5, scy),
        xytext=(acx + 2.2, acy),
        arrowprops=dict(arrowstyle='->', color=c_match, lw=1.8))

# appel 4 → erreur
ax.annotate('',
    xy=(4.5, 1.48),
    xytext=(appels[3][0] + 2.2, appels[3][1]),
    arrowprops=dict(arrowstyle='->', color=c_err, lw=1.8,
                    linestyle='dashed'))

# Surcharges → implémentation (flèches vers le bas)
for cx, cy, _, color in surcharges:
    ax.annotate('',
        xy=(8.0, impl_y + 0.6),
        xytext=(cx, cy - 0.38),
        arrowprops=dict(arrowstyle='->', color=c_impl, lw=1.2,
                        linestyle='dotted'))

# Légende
lx, ly = 0.3, 3.0
ax.text(lx, ly, 'Légende', fontsize=9, fontweight='bold', color='#444')
items = [
    (c_match, '✓ Surcharge correspondante'),
    (c_err,   '✗ Aucune correspondance'),
    (c_impl,  "Signature d'implémentation (non exposée)"),
]
for i, (color, label) in enumerate(items):
    r = patches.FancyBboxPatch(
        (lx, ly - 0.55 - i * 0.55 - 0.18), 0.25, 0.32,
        boxstyle="round,pad=0.04", linewidth=1.5,
        edgecolor=color, facecolor=color, alpha=0.4
    )
    ax.add_patch(r)
    ax.text(lx + 0.35, ly - 0.55 - i * 0.55,
            label, ha='left', va='center',
            fontsize=8.5, color='#333')

plt.tight_layout()
plt.show()
_images/01a04b92b8713c0cd1c8cc3f30b610db8530ee35db1b8d3e2d681a640dbb0bbe.png

Fonctions génériques#

Introduction aux génériques#

Un générique permet d’écrire une fonction qui fonctionne sur différents types tout en conservant une relation entre les types des paramètres et du retour. La variable de type (par convention une lettre majuscule comme T) est déclarée entre chevrons <> avant la liste des paramètres :

// Sans générique : perd l'information de type
function premierÉlément(tableau: any[]): any {
  return tableau[0];
}

// Avec générique : préserve le type
function premierÉlément<T>(tableau: T[]): T | undefined {
  return tableau[0];
}

const s = premierÉlément(["Alice", "Bob"]); // s : string | undefined
const n = premierÉlément([1, 2, 3]);        // n : number | undefined

TypeScript infère automatiquement l’argument de type depuis les arguments de la fonction. On peut aussi le préciser explicitement : premierÉlément<string>(["Alice", "Bob"]).

Contraintes génériques#

On peut contraindre un type générique avec extends pour s’assurer qu’il possède certaines propriétés :

interface AvecLongueur {
  length: number;
}

function journaliserLongueur<T extends AvecLongueur>(valeur: T): T {
  console.log(`Longueur : ${valeur.length}`);
  return valeur;
}

journaliserLongueur("bonjour");     // OK : string a une propriété length
journaliserLongueur([1, 2, 3]);    // OK : Array a une propriété length
journaliserLongueur({ length: 10 }); // OK : l'objet satisfait la contrainte
journaliserLongueur(42);            // Erreur : number n'a pas de propriété length

Exemple 3 (Générique avec plusieurs variables de type)

On peut déclarer plusieurs variables de type pour exprimer des relations entre plusieurs paramètres :

function transformer<Entrée, Sortie>(
  tableau: Entrée[],
  transformateur: (élément: Entrée) => Sortie
): Sortie[] {
  return tableau.map(transformateur);
}

// TypeScript infère Entrée = string, Sortie = number
const longueurs = transformer(
  ["alice", "bob", "clara"],
  s => s.length
); // number[]

// TypeScript infère Entrée = number, Sortie = string
const chaînes = transformer(
  [1, 2, 3],
  n => n.toString()
); // string[]

## `this` dans les fonctions

### Le problème de `this` en JavaScript

En JavaScript, la valeur de `this` dans une fonction dépend de **la façon dont la fonction est appelée**, pas de l'endroit où elle est définie. Ce comportement est une source fréquente de bugs :

```typescript
class Minuterie {
  décompte: number = 10;

  démarrer() {
    setInterval(function() {
      // Ici, `this` n'est PAS la Minuterie — c'est `undefined` en mode strict
      this.décompte--; // Erreur à l'exécution !
    }, 1000);
  }
}
```

### Le paramètre fictif `this: Type`

TypeScript permet de déclarer un paramètre fictif `this` en première position pour annoter le type que `this` doit avoir lors de l'appel. Ce paramètre est **effacé à la compilation** et n'apparaît pas dans la liste des arguments réels :

```typescript
interface Minuterie {
  décompte: number;
  démarrer(this: Minuterie): void;
}

function tic(this: Minuterie): void {
  this.décompte--; // TypeScript sait que this : Minuterie
  if (this.décompte === 0) console.log("Terminé !");
}

const m: Minuterie = { décompte: 5, démarrer: tic };
m.démarrer(); // OK
tic();        // Erreur : 'this' est de type 'void'
              // (la fonction est appelée sans contexte)
```

### Fonctions fléchées et `this` lexical

Les fonctions fléchées ne possèdent pas leur propre `this` : elles capturent le `this` de leur contexte lexical englobant. C'est la solution idiomatique au problème des callbacks dans les classes :

```typescript
class Minuterie {
  décompte: number = 10;

  démarrer(): void {
    // La fonction fléchée capture le `this` de `démarrer`
    setInterval(() => {
      this.décompte--; // `this` est bien la Minuterie
      if (this.décompte === 0) console.log("Terminé !");
    }, 1000);
  }
}
```

TypeScript comprend cette sémantique et infère correctement le type de `this` dans les fonctions fléchées.

## Types de fonctions avancés

### `void` vs `undefined`

La distinction entre `void` et `undefined` comme type de retour est subtile mais importante.

`void` signifie que la valeur de retour **sera ignorée**. C'est la convention pour les callbacks : un callback typé `() => void` peut en pratique retourner n'importe quelle valeur — TypeScript l'autorise parce que cette valeur sera ignorée par l'appelant :

```typescript
type Callback = () => void;

// Ces fonctions satisfont toutes le type Callback
const cb1: Callback = () => {};
const cb2: Callback = () => true;    // OK même si le retour est boolean
const cb3: Callback = () => 42;      // OK même si le retour est number

// En revanche, une fonction dont le retour est déclaré void
// ne peut pas retourner une valeur utilisable :
function journaliser(): void {
  console.log("journal");
  // return 42; // Erreur : Type 'number' is not assignable to type 'void'
}
```

`undefined` comme type de retour est plus strict : la fonction doit explicitement retourner `undefined` ou ne rien retourner.

### `never` dans les fonctions qui échouent toujours

Une fonction dont le type de retour est `never` **ne termine jamais normalement** : elle lance toujours une exception, ou contient une boucle infinie. TypeScript utilise ce type pour indiquer qu'un point de code est inatteignable :

```typescript
function paniquer(message: string): never {
  throw new Error(`Erreur fatale : ${message}`);
}

function vérifier(valeur: unknown): asserts valeur is string {
  if (typeof valeur !== 'string') {
    paniquer(`Attendu une chaîne, reçu ${typeof valeur}`);
  }
}
```

Le type `asserts valeur is Type` est une **signature d'assertion** : après l'appel à `vérifier(x)`, TypeScript sait que `x` est du type asserté si la fonction n'a pas lancé d'exception.

### Callbacks typés

Typer correctement les callbacks améliore la lisibilité et la robustesse du code. On peut utiliser des types de fonctions dans les interfaces :

```typescript
interface OptionsRequête {
  url: string;
  méthode?: 'GET' | 'POST' | 'PUT' | 'DELETE';
  onSuccès: (données: unknown) => void;
  onErreur: (erreur: Error) => void;
  onTerminaison?: () => void;
}

function requête(options: OptionsRequête): void {
  fetch(options.url, { method: options.méthode ?? 'GET' })
    .then(r => r.json())
    .then(options.onSuccès)
    .catch(options.onErreur)
    .finally(options.onTerminaison);
}

requête({
  url: "https://api.exemple.fr/données",
  onSuccès: données => console.log(données),
  onErreur: err => console.error(err.message),
});
```

### Fonctions qui retournent des fonctions

TypeScript gère parfaitement les **fonctions d'ordre supérieur** qui retournent d'autres fonctions, y compris en présence de génériques :

```typescript
// Fabrique de comparateurs
function comparerPar<T, K extends keyof T>(
  clé: K
): (a: T, b: T) => number {
  return (a, b) => {
    if (a[clé] < b[clé]) return -1;
    if (a[clé] > b[clé]) return 1;
    return 0;
  };
}

const utilisateurs = [
  { nom: "Clara", âge: 32 },
  { nom: "Alice", âge: 28 },
  { nom: "Bob",   âge: 35 },
];

utilisateurs.sort(comparerPar('nom'));
// [{ nom: "Alice", ... }, { nom: "Bob", ... }, { nom: "Clara", ... }]

utilisateurs.sort(comparerPar('âge'));
// [{ nom: "Alice", âge: 28 }, ...]
```

TypeScript vérifie que `'nom'` et `'âge'` sont bien des clés de l'objet passé en argument. Si on essayait `comparerPar('inexistante')`, on obtiendrait une erreur à la compilation.

```{prf:example} Curryfication typée
:label: example-04-02
La curryfication consiste à transformer une fonction à plusieurs paramètres en une série de fonctions à un seul paramètre. TypeScript type correctement cette transformation :

```typescript
// Fonction currifiée à deux niveaux
function curry<A, B, C>(
  f: (a: A, b: B) => C
): (a: A) => (b: B) => C {
  return a => b => f(a, b);
}

const additionner = (a: number, b: number) => a + b;
const ajouterCinq = curry(additionner)(5); // (b: number) => number

ajouterCinq(3);  // 8
ajouterCinq(10); // 15
```

Résumé#

Dans ce chapitre, nous avons exploré en profondeur le typage des fonctions en TypeScript :

  • L”annotation des paramètres et du type de retour est la base. L’inférence du type de retour est puissante, mais annoter les fonctions publiques est une bonne pratique. Le type d’une fonction comme valeur s’écrit (param: Type) => TypeRetour.

  • Les paramètres optionnels (param?: Type) et les valeurs par défaut (param = valeur) permettent de définir des fonctions flexibles. Les paramètres optionnels doivent toujours venir après les paramètres obligatoires.

  • Les paramètres rest (...args: T[]) capturent un nombre variable d’arguments dans un tableau, et l’opérateur spread (...tableau) étale un tableau comme arguments d’un appel.

  • La surcharge de fonctions permet de déclarer plusieurs signatures valides pour une même fonction. TypeScript choisit la signature correspondante à l’appel ; la signature d’implémentation, plus large, est interne et non exposée.

  • Les génériques (<T>) permettent d’écrire des fonctions polymorphes qui préservent les relations entre les types des paramètres et du retour. Les contraintes (T extends Interface) restreignent les types acceptés.

  • Le paramètre fictif this: Type annote le type de this dans une méthode et protège contre les appels dans le mauvais contexte. Les fonctions fléchées capturent this lexicalement et sont la solution idiomatique pour les callbacks dans les classes.

  • void comme type de retour signifie que la valeur de retour sera ignorée (pratique pour les callbacks). never indique une fonction qui ne termine jamais normalement. Les signatures d’assertion (asserts x is Type) permettent de typer des fonctions de validation qui narrowent le type après leur appel.

Dans le chapitre suivant, nous aborderons les classes et la programmation orientée objet en TypeScript : constructeurs, modificateurs d’accès, classes abstraites, interfaces implémentées par des classes et décorateurs.