Le système de types#

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 types primitifs#

Le système de types de TypeScript s’ancre dans les valeurs que JavaScript peut manipuler nativement. TypeScript reconnaît huit types primitifs qui correspondent directement aux types de valeurs de JavaScript.

string#

Le type string représente toutes les chaînes de caractères, qu’elles soient délimitées par des guillemets simples, doubles ou par des gabarits de chaîne (template literals) :

const prenom: string = "Alice";
const salutation: string = `Bonjour, ${prenom} !`;
const description: string = 'Développeuse TypeScript';

number#

TypeScript, comme JavaScript, n’opère pas de distinction entre les entiers et les nombres à virgule flottante. Il n’existe qu’un seul type numérique, number, qui couvre également les valeurs spéciales NaN et Infinity :

const age: number = 30;
const prix: number = 19.99;
const hexadecimal: number = 0xFF;
const infini: number = Infinity;

boolean#

Le type boolean ne peut prendre que deux valeurs : true ou false. Il est fondamental pour les conditions et les gardes de flux de contrôle :

const estActif: boolean = true;
const aPayé: boolean = false;

null et undefined#

En TypeScript avec "strictNullChecks": true (inclus dans "strict": true), null et undefined sont des types distincts, non assignables à d’autres types sans déclaration explicite. C’est l’un des apports les plus importants de TypeScript par rapport à JavaScript.

let valeurNulle: null = null;
let valeurIndéfinie: undefined = undefined;

// Avec strictNullChecks activé :
let nom: string = null; // Erreur ! null n'est pas assignable à string
let nom2: string | null = null; // Correct : union explicite

```{prf:definition} strictNullChecks :label: definition-02-01 Avec "strictNullChecks": true, null et undefined constituent leurs propres types et ne peuvent pas être assignés silencieusement à d’autres types. Sans cette option, null et undefined sont des sous-types de tous les types, ce qui reproduit le comportement de JavaScript mais laisse passer des erreurs de type « Cannot read properties of null » (null pointer exceptions) très fréquentes. Activer strictNullChecks est l’une des mesures les plus efficaces pour éliminer toute une classe de bugs à l’exécution.


### `symbol`

Le type `symbol` correspond aux valeurs créées par `Symbol()`. Les symboles sont des valeurs **uniques et non comparables** entre elles, utilisées principalement comme clés de propriétés d'objets pour éviter les collisions de noms :

```typescript
const idSession: symbol = Symbol('session');
const idSession2: symbol = Symbol('session');
// idSession === idSession2 → false, même libellé, valeurs distinctes

bigint#

Le type bigint permet de représenter des entiers arbitrairement grands, au-delà de la limite sûre de number (Number.MAX_SAFE_INTEGER = 2⁵³ - 1). Les littéraux bigint s’écrivent avec le suffixe n :

const grandeValeur: bigint = 9007199254740993n;
const résultat: bigint = grandeValeur * 2n;

void#

void représente l”absence de valeur retournée par une fonction. C’est le type de retour des fonctions qui ne retournent rien (ou qui retournent implicitement undefined) :

function journaliser(message: string): void {
  console.log(message);
  // Pas de return, ou return; sans valeur
}

Annotation de types#

Annotation explicite vs inférence#

En TypeScript, on peut déclarer le type d’une variable de deux façons. L”annotation explicite consiste à écrire le type après le nom de la variable, séparé par un deux-points :

const compteur: number = 0;
const utilisateurs: string[] = [];

L”inférence automatique consiste à laisser TypeScript déduire le type depuis la valeur initiale. Dans cet exemple, TypeScript sait que compteur est un number sans qu’on ait à le préciser :

const compteur = 0;           // TypeScript infère : number
const utilisateurs = [];      // TypeScript infère : never[] (attention !)
const nom = "Alice";          // TypeScript infère : string
const actif = true;           // TypeScript infère : boolean

Remarque 2

La règle de style recommandée par la communauté TypeScript est de préférer l’inférence lorsqu’elle est évidente et correcte, et de n’écrire des annotations explicites que lorsque l’inférence ne peut pas deviner le bon type, ou lorsque l’annotation améliore la lisibilité. Annoter const nom: string = "Alice" est redondant et alourdit le code sans apporter de bénéfice. En revanche, annoter le type de retour d’une fonction publique est une bonne pratique : cela documente l’intention et protège contre des changements accidentels.

L’inférence de types#

L’inférence de types est l’un des points forts de TypeScript : le compilateur peut souvent déterminer le type d’une expression sans annotation explicite, à partir du contexte.

Inférence sur les variables#

Quand on initialise une variable avec une valeur, TypeScript en déduit le type :

let x = 42;         // x : number
let s = "bonjour";  // s : string
let b = true;       // b : boolean

// TypeScript interdit ensuite les réassignations de type différent :
x = "texte"; // Erreur : Type 'string' is not assignable to type 'number'

Inférence sur les fonctions#

TypeScript infère automatiquement le type de retour d’une fonction à partir des expressions return qu’elle contient :

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

function préfixer(s: string) {
  return "Préfixe_" + s; // TypeScript infère le retour : string
}

Si une fonction a plusieurs branches return de types différents, TypeScript infère l’union de ces types.

Inférence contextuelle#

TypeScript peut inférer le type d’une expression depuis son contexte d’utilisation. C’est particulièrement visible avec les fonctions passées en argument :

const nombres = [1, 2, 3, 4, 5];

// TypeScript sait que 'n' est un number grâce au type du tableau
const doubles = nombres.map(n => n * 2);

// TypeScript sait que 'e' est un MouseEvent grâce au type du gestionnaire
document.addEventListener('click', e => {
  console.log(e.clientX); // e est inféré comme MouseEvent
});

Limites de l’inférence#

L’inférence a ses limites. Lorsqu’une variable est déclarée sans initialisation, ou lorsqu’un tableau vide est déclaré sans annotation, TypeScript ne peut pas inférer un type utile :

let valeur;       // valeur : any — TypeScript abdique
let liste = [];   // liste : never[] — tableau qui ne peut rien contenir

// Solution : annoter explicitement
let valeur: number;
let liste: string[] = [];

any, unknown, never#

Ces trois types occupent des positions particulières dans la hiérarchie des types de TypeScript.

any : la porte de sortie#

Le type any désactive toutes les vérifications de types. Une variable de type any peut recevoir n’importe quelle valeur, et on peut appeler n’importe quelle propriété ou méthode dessus sans erreur de compilation :

let x: any = 42;
x = "texte";        // OK
x = { foo: "bar" }; // OK
x.méthodeInexistante(); // Pas d'erreur à la compilation — mais une erreur à l'exécution !

Remarque 3

any est parfois qualifié de « porte de sortie » (escape hatch) du système de types. Il existe des cas légitimes pour son utilisation : intégration avec du code JavaScript non typé, prototypage rapide ou gestion de données vraiment dynamiques. Mais dans un projet TypeScript bien écrit, l’utilisation de any doit être explicite, minimale et documentée. Le mode strict, via noImplicitAny, interdit le recours implicite à any et oblige à l’écrire explicitement, ce qui rend chaque usage volontaire et traçable.

unknown : le any sûr#

Le type unknown est la version sûre de any. Comme any, une variable de type unknown peut recevoir n’importe quelle valeur. Mais contrairement à any, on ne peut pas l’utiliser directement sans d’abord vérifier son type :

let entrée: unknown = JSON.parse(texteExterne);

// Erreur : on ne peut pas appeler une méthode sur unknown sans vérification
entrée.toUpperCase(); // Erreur TS !

// Correct : on vérifie le type avant d'utiliser la valeur
if (typeof entrée === 'string') {
  console.log(entrée.toUpperCase()); // OK : TypeScript sait que c'est une string
}

unknown est le type recommandé pour les données provenant de l’extérieur du programme (résultats d’API, entrées utilisateur, JSON parsé) : il force une validation explicite avant toute utilisation.

never : le type inhabité#

Le type never représente un ensemble vide de valeurs : aucune valeur ne peut avoir le type never. Il apparaît dans deux contextes principaux.

Fonctions qui ne retournent jamais (elles lancent toujours une exception ou bouclent à l’infini) :

function lancerErreur(message: string): never {
  throw new Error(message);
  // Cette ligne est inatteignable
}

function boucleInfinie(): never {
  while (true) { /* ... */ }
}

Branche impossible dans un narrowing exhaustif : quand TypeScript a éliminé toutes les possibilités d’une union, le type résiduel est never. C’est utile pour s’assurer que tous les cas ont été traités :

type Forme = 'cercle' | 'carré' | 'triangle';

function décrire(forme: Forme): string {
  switch (forme) {
    case 'cercle':   return 'Une forme ronde';
    case 'carré':    return 'Une forme à angles droits';
    case 'triangle': return 'Une forme à trois côtés';
    default:
      // Si on ajoute un nouveau cas à Forme sans mettre à jour ce switch,
      // TypeScript signalera une erreur ici
      const _exhaustif: never = forme;
      throw new Error(`Forme inconnue : ${_exhaustif}`);
  }
}

Assertions de types#

Parfois, on dispose d’informations sur un type que le compilateur TypeScript ne peut pas déduire automatiquement. Les assertions de types permettent de dire au compilateur : « Fais-moi confiance, je sais que cette valeur est de ce type. »

Syntaxe as Type#

La syntaxe préférée est le mot-clé as :

const entrée: unknown = obtenirDonnées();
const texte = entrée as string; // On affirme que c'est une string
console.log(texte.toUpperCase());

Syntaxe <Type> (ancienne)#

La syntaxe alternative avec des chevrons est équivalente, mais elle est déconseillée dans les fichiers .tsx (React avec JSX) car elle est ambiguë avec la syntaxe JSX :

const texte = <string>entrée; // Équivalent, mais éviter dans .tsx

as const#

L’assertion as const est particulièrement utile : elle dit au compilateur de traiter une valeur comme une constante littérale immuable, plutôt que d’élargir son type vers le type primitif général :

const direction = "nord";
// TypeScript infère : string (type élargi)

const direction2 = "nord" as const;
// TypeScript infère : "nord" (type littéral exact)

const config = {
  hôte: "localhost",
  port: 8080,
} as const;
// TypeScript infère : { readonly hôte: "localhost"; readonly port: 8080 }

Remarque 4

Les assertions de types ne sont pas des conversions et ne modifient pas la valeur à l’exécution. Elles indiquent simplement au compilateur de traiter la valeur comme ayant le type asserté. Si l’assertion est incorrecte, l’erreur se produira à l’exécution, pas à la compilation. C’est pourquoi on les utilise avec parcimonie, uniquement quand on est certain de l’information que le compilateur ne peut pas inférer seul — et jamais pour « faire taire » le compilateur sans comprendre pourquoi il se plaint.

Narrowing#

Le narrowing (ou rétrécissement de type) est le mécanisme par lequel TypeScript affine le type d’une valeur à l’intérieur d’un bloc de code, après qu’une vérification a été effectuée. C’est l’une des fonctionnalités les plus puissantes du système de types de TypeScript.

typeof#

L’opérateur typeof de JavaScript retourne une chaîne décrivant le type d’une valeur. TypeScript reconnaît ces vérifications et affine le type dans la branche correspondante :

function formater(valeur: string | number): string {
  if (typeof valeur === 'string') {
    // Ici, TypeScript sait que valeur : string
    return valeur.toUpperCase();
  } else {
    // Ici, TypeScript sait que valeur : number
    return valeur.toFixed(2);
  }
}

instanceof#

Pour les instances de classes, instanceof permet un narrowing vers le type de la classe :

class Chat {
  ronronner() { return "Prrrr..."; }
}

class Chien {
  aboyer() { return "Wouf !"; }
}

function faireParler(animal: Chat | Chien): string {
  if (animal instanceof Chat) {
    return animal.ronronner(); // animal : Chat
  } else {
    return animal.aboyer();   // animal : Chien
  }
}

L’opérateur in#

L’opérateur in vérifie si une propriété existe sur un objet. TypeScript l’utilise pour affiner les types d’objets :

interface Cercle { rayon: number; }
interface Rectangle { largeur: number; hauteur: number; }

function aire(forme: Cercle | Rectangle): number {
  if ('rayon' in forme) {
    return Math.PI * forme.rayon ** 2; // forme : Cercle
  } else {
    return forme.largeur * forme.hauteur; // forme : Rectangle
  }
}

Gardes de types personnalisées#

Pour des cas plus complexes, on peut définir des fonctions de garde de type (type predicates). Leur type de retour prend la forme paramètre is Type :

interface Utilisateur {
  nom: string;
  email: string;
}

function estUtilisateur(valeur: unknown): valeur is Utilisateur {
  return (
    typeof valeur === 'object' &&
    valeur !== null &&
    'nom' in valeur &&
    'email' in valeur &&
    typeof (valeur as any).nom === 'string' &&
    typeof (valeur as any).email === 'string'
  );
}

function traiter(données: unknown): void {
  if (estUtilisateur(données)) {
    // données : Utilisateur — TypeScript le sait grâce au prédicat
    console.log(données.email.toLowerCase());
  }
}

Analyse de flux de contrôle#

TypeScript effectue une analyse de flux de contrôle (control flow analysis) sophistiquée : il suit les vérifications effectuées dans les conditions, les switch, les return anticipés et les throw pour savoir précisément quel type est possible à chaque point du programme :

function longueur(valeur: string | null | undefined): number {
  if (valeur == null) {
    // Ici, valeur : null | undefined
    return 0;
  }
  // Ici, TypeScript sait que valeur : string (null et undefined éliminés)
  return valeur.length;
}

Visualisation : hiérarchie des types TypeScript#

Hide code cell source

fig, ax = plt.subplots(figsize=(14, 10))
ax.set_xlim(-0.5, 14)
ax.set_ylim(-0.5, 10)
ax.axis('off')
ax.set_title('Hiérarchie des types TypeScript (treillis simplifié)',
             fontsize=15, fontweight='bold', pad=16)

palette = sns.color_palette("muted", 10)

c_unknown = '#2c3e50'
c_any     = '#e74c3c'
c_obj     = '#2980b9'
c_prim    = '#27ae60'
c_never   = '#8e44ad'
c_arrow   = '#7f8c8d'

bw, bh = 2.2, 0.7

def box(ax, cx, cy, label, color, width=None, alpha=0.2):
    w = width if width else bw
    rect = patches.FancyBboxPatch(
        (cx - w / 2, cy - bh / 2), w, bh,
        boxstyle="round,pad=0.1", linewidth=2,
        edgecolor=color, facecolor=color, alpha=alpha
    )
    ax.add_patch(rect)
    border = patches.FancyBboxPatch(
        (cx - w / 2, cy - bh / 2), w, bh,
        boxstyle="round,pad=0.1", linewidth=2,
        edgecolor=color, facecolor='none'
    )
    ax.add_patch(border)
    ax.text(cx, cy, label, ha='center', va='center',
            fontsize=10, fontweight='bold', color=color)

def arr(ax, x1, y1, x2, y2, color='#7f8c8d', lw=1.8):
    ax.annotate('',
        xy=(x2, y2 + bh / 2 + 0.05),
        xytext=(x1, y1 - bh / 2 - 0.05),
        arrowprops=dict(arrowstyle='->', color=color, lw=lw))

# Niveau 5 — unknown (sommet)
box(ax, 7, 9.2, 'unknown', c_unknown, width=2.6, alpha=0.25)
ax.text(12.5, 9.2, 'Type sommet :\ntout y est assignable',
        ha='left', va='center', fontsize=8.5, color=c_unknown, style='italic')

# Niveau 4 — any (à part)
box(ax, 2.0, 7.2, 'any', c_any, width=2.0, alpha=0.2)
ax.text(0.1, 7.2, 'Porte de sortie :\ncontourne les vérifications',
        ha='left', va='center', fontsize=8.5, color=c_any, style='italic')

# Niveau 3 — object, string | number | ..., Function
box(ax, 7, 7.2, 'object', c_obj, width=2.4)
box(ax, 10.8, 7.2, 'Function', c_obj, width=2.2)

# Lignes de unknown vers les sous-types
for cx in [7.0, 10.8]:
    arr(ax, 7, 9.2, cx, 7.2, c_unknown, lw=1.5)

# Niveau 2 — types primitifs + classes
prims = [
    (3.2,  5.0, 'string',    c_prim),
    (5.6,  5.0, 'number',    c_prim),
    (7.0,  5.0, 'boolean',   c_prim),
    (8.6,  5.0, 'symbol',    c_prim),
    (10.2, 5.0, 'bigint',    c_prim),
    (11.8, 5.0, 'null',      c_prim),
    (4.4,  3.2, 'undefined', c_prim),
]

for cx, cy, label, color in prims:
    box(ax, cx, cy, label, color, width=2.0)

# Connexions object → primitifs (sauf null/undefined)
arr(ax, 7, 7.2, 3.2,  5.0, c_obj, lw=1.3)
arr(ax, 7, 7.2, 5.6,  5.0, c_obj, lw=1.3)
arr(ax, 7, 7.2, 7.0,  5.0, c_obj, lw=1.3)
arr(ax, 7, 7.2, 8.6,  5.0, c_obj, lw=1.3)
arr(ax, 7, 7.2, 10.2, 5.0, c_obj, lw=1.3)
arr(ax, 7, 7.2, 11.8, 5.0, c_obj, lw=1.3)
arr(ax, 7, 7.2, 4.4,  3.2, c_obj, lw=1.3)

# Types littéraux sous string, number, boolean
lits = [
    (2.2, 3.2, '"alice"',  c_prim),
    (3.8, 3.2, '"nord"',   c_prim),
    (5.2, 3.2, '42',       c_prim),
    (6.4, 3.2, '3.14',     c_prim),
    (7.6, 3.2, 'true',     c_prim),
    (8.8, 3.2, 'false',    c_prim),
]

for cx, cy, label, color in lits:
    box(ax, cx, cy, label, color, width=1.6, alpha=0.12)

arr(ax, 3.2, 5.0, 2.2, 3.2, c_prim, lw=1.1)
arr(ax, 3.2, 5.0, 3.8, 3.2, c_prim, lw=1.1)
arr(ax, 5.6, 5.0, 5.2, 3.2, c_prim, lw=1.1)
arr(ax, 5.6, 5.0, 6.4, 3.2, c_prim, lw=1.1)
arr(ax, 7.0, 5.0, 7.6, 3.2, c_prim, lw=1.1)
arr(ax, 7.0, 5.0, 8.8, 3.2, c_prim, lw=1.1)

# Niveau 0 — never (bas)
box(ax, 7, 1.4, 'never', c_never, width=2.4, alpha=0.2)
ax.text(10.0, 1.4, 'Type bas : assignable\nà tout, inhabité',
        ha='left', va='center', fontsize=8.5, color=c_never, style='italic')

# never → tous les types littéraux / primitifs
for cx, cy, _, _ in lits:
    arr(ax, 7, 1.4, cx, cy, c_never, lw=1.1)
arr(ax, 7, 1.4, 4.4, 3.2, c_never, lw=1.1)
arr(ax, 7, 1.4, 11.8, 5.0, c_never, lw=1.1)

# Légende
legend_x = 0.2
legend_y = 2.5
items = [
    (c_unknown, 'Type sommet (unknown)'),
    (c_any,     'Type spécial (any)'),
    (c_obj,     'Types objets / classes'),
    (c_prim,    'Types primitifs / littéraux'),
    (c_never,   'Type bas (never)'),
]
ax.text(legend_x, legend_y + 0.8, 'Légende', fontsize=10,
        fontweight='bold', color='#333333')
for i, (color, label) in enumerate(items):
    rect = patches.FancyBboxPatch(
        (legend_x, legend_y - i * 0.52 - 0.22), 0.28, 0.34,
        boxstyle="round,pad=0.05", linewidth=1.5,
        edgecolor=color, facecolor=color, alpha=0.4
    )
    ax.add_patch(rect)
    ax.text(legend_x + 0.4, legend_y - i * 0.52,
            label, ha='left', va='center',
            fontsize=8.5, color='#333333')

plt.tight_layout()
plt.show()
_images/6d015772140e43be3c78a83a025cb4b86cc1b429487dfd46924b1fb9f5c2a238.png

Résumé#

Dans ce chapitre, nous avons exploré le cœur du système de types de TypeScript :

  • TypeScript reconnaît huit types primitifs : string, number, boolean, null, undefined, symbol, bigint et void. Avec strictNullChecks, null et undefined ne peuvent plus se glisser silencieusement là où ils ne sont pas attendus.

  • L”annotation explicite et l”inférence automatique sont complémentaires. La règle d’or est de préférer l’inférence lorsqu’elle est évidente, et d’annoter lorsqu’elle apporte de la clarté ou lorsque le compilateur en a besoin.

  • any désactive toutes les vérifications et doit être évité. unknown est son alternative sûre : il accepte toutes les valeurs mais exige une vérification de type avant utilisation. never représente l’ensemble vide des valeurs et est utilisé pour les fonctions qui ne retournent jamais et pour l’exhaustivité des switch.

  • Les assertions de types (as Type, as const) permettent de fournir au compilateur des informations qu’il ne peut pas déduire seul. Elles doivent être utilisées avec parcimonie.

  • Le narrowing est le mécanisme par lequel TypeScript affine le type d’une valeur après une vérification. Il s’appuie sur typeof, instanceof, l’opérateur in, les gardes de types personnalisées et l’analyse de flux de contrôle.

Dans le chapitre suivant, nous aborderons les types composés : tableaux, tuples, interfaces, alias de types, unions, intersections, types littéraux et énumérations — les briques fondamentales pour construire des structures de données complexes et précisément typées.