TypeScript

Types et inférence

Le système de types et ce qu'il déduit seul.

Vérifié en septembre 2026 · TypeScript 6.0 · environ 14 min

TypeScript vérifie un programme JavaScript sans rien changer à ce qu'il exécute : les types existent pendant la compilation, puis disparaissent. Pour que ce contrôle reste léger, le compilateur déduit l'essentiel seul — le type d'une variable, le retour d'une fonction, le paramètre d'un rappel — et n'attend d'annotation qu'aux frontières : paramètres, données venues de l'extérieur, contrats publics. Savoir ce qu'il déduit, et à quel moment il élargit ou restreint un type, c'est savoir où une annotation est utile et où elle efface de l'information. Ce cours pose ces bases ; les génériques, les types conditionnels et les unions discriminées sont traités dans le cours Types avancés.

Un typage structurel

En C#, une classe n'est acceptée à la place d'une interface que si elle la déclare : le typage est nominal, fondé sur les noms et les déclarations. TypeScript compare des formes. Un type A est assignable à B dès que A possède toutes les propriétés de B, avec des types compatibles ; qu'il en ait d'autres ne gêne pas. Ce choix découle de JavaScript lui-même, où les objets naissent le plus souvent de littéraux ou de JSON, sans classe ni déclaration à laquelle se rattacher.

interface Point {
  x: number;
  y: number;
}

// Pixel ne mentionne Point nulle part : ni implements, ni heritage.
class Pixel {
  constructor(
    public x: number,
    public y: number,
    public couleur: string,
  ) {}
}

function distance(p: Point): number {
  return Math.hypot(p.x, p.y);
}

// Accepte : un Pixel a au moins x et y, du bon type. Son nom ne compte pas.
console.log(distance(new Pixel(3, 4, 'rouge'))); // 5
console.log(distance({ x: 6, y: 8 })); // 10

// Un membre prive (# ou private) rend la classe nominale : seule une instance
// de cette classe, ou d'une classe derivee, porte ce champ-la.
class Compte {
  #solde = 0;
  crediter(montant: number): void {
    this.#solde += montant;
  }
}

class Imitation {
  crediter(montant: number): void {}
}

// const compte: Compte = new Imitation();
// error TS2741: Property '#solde' is missing in type 'Imitation' but required in type 'Compte'.

L'exception vient des membres privés. Un champ #solde ou un membre private n'appartient qu'à la classe qui le déclare : aucune autre forme ne peut le reproduire, et la classe redevient de fait nominale.

Le revers de la règle se paie avec les alias. type Euros = number ne crée pas de type, seulement un nom de plus pour number, et le compilateur ne distingue pas deux alias de la même forme.

// Un alias ne cree pas de type : Euros et Dollars sont deux noms de number.
type Euros = number;
type Dollars = number;

function enDollars(montant: Euros): Dollars {
  return montant * 1.25; // taux fixe pour l'exemple
}

const prixUs: Dollars = 40;

// Compile sans un mot : un montant deja en dollars est converti une seconde fois.
console.log(enDollars(prixUs)); // 50

Pour obtenir l'équivalent d'un record struct Euros, on ajoute au type une marque : une propriété fictive, différente pour chaque devise, que rien ne porte à l'exécution. Les deux types ne sont plus de la même forme, et le mélange devient une erreur. Le prix est un as à l'endroit où la valeur est créée, qu'il vaut mieux concentrer dans une seule fonction.

// Une marque : une propriete qui n'existe que dans les types. Aucune valeur ne
// la porte a l'execution ; elle suffit a rendre les deux types incompatibles.
type Euros = number & { readonly __devise: 'EUR' };
type Dollars = number & { readonly __devise: 'USD' };

// Les seuls endroits ou un number devient un montant : la marque s'appose par as.
const euros = (valeur: number) => valeur as Euros;
const dollars = (valeur: number) => valeur as Dollars;

function enDollars(montant: Euros): Dollars {
  return dollars(montant * 1.25);
}

const prixUs = dollars(40);

// enDollars(prixUs);
// error TS2345: Argument of type 'Dollars' is not assignable to parameter of type 'Euros'.
//   Type 'Dollars' is not assignable to type '{ readonly __devise: "EUR"; }'.
//     Types of property '__devise' are incompatible.
//       Type '"USD"' is not assignable to type '"EUR"'.

// enDollars(40);
// error TS2345: Argument of type 'number' is not assignable to parameter of type 'Euros'.
//   Type 'number' is not assignable to type '{ readonly __devise: "EUR"; }'.

console.log(enDollars(euros(40))); // 50

// Un montant reste un number : l'arithmetique est permise, et rend un number.
const total: number = prixUs + enDollars(euros(40));

any, unknown et never

any n'est pas un type au sens du contrôle : il désactive la vérification. Une valeur any s'affecte à tout, accepte tout accès, et chaque accès rend un nouveau any, si bien que l'absence de contrôle se propage bien au-delà de la ligne qui l'a introduite. JSON.parse rend précisément un any.

interface Client {
  nom: string;
  adresse: { ville: string };
}

// JSON.parse rend any : le compilateur cesse de verifier tout ce qui en decoule.
const client = JSON.parse('{"nom":"Durand"}'); // any

// Chaque acces sur un any rend un any, sans controle : ni la faute de frappe
// ni la propriete absente ne sont signalees.
const nom = client.nmo; // any

// any s'affecte a tout : le type Client est affirme, jamais verifie.
const verifie: Client = client;
console.log(nom, verifie.nom); // undefined Durand

// Le compilateur tient verifie.adresse pour present ; il ne l'est pas.
console.log(verifie.adresse.ville);
// A l'execution (Node, Chrome) : TypeError: Cannot read properties of undefined (reading 'ville')

unknown accepte les mêmes valeurs, mais n'autorise rien tant que la valeur n'a pas été examinée : ni accès, ni appel, ni affectation à un type plus précis. Chaque test restreint le type, et le compilateur suit ces tests jusqu'à la forme attendue. C'est le type qui convient à toute donnée venue de l'extérieur, et c'est celui que strict donne à la variable d'un catch, par l'option useUnknownInCatchVariables.

interface Client {
  nom: string;
  ville: string;
}

function lireClient(texte: string): Client {
  // unknown : n'importe quelle valeur peut arriver, mais rien n'est permis
  // tant qu'elle n'a pas ete examinee.
  const donnees: unknown = JSON.parse(texte);

  // const client: Client = donnees;
  // error TS2322: Type 'unknown' is not assignable to type 'Client'.

  // console.log(donnees.nom);
  // error TS18046: 'donnees' is of type 'unknown'.

  if (
    typeof donnees === 'object' &&
    donnees !== null &&
    'nom' in donnees &&
    typeof donnees.nom === 'string' &&
    'ville' in donnees &&
    typeof donnees.ville === 'string'
  ) {
    return { nom: donnees.nom, ville: donnees.ville };
  }
  throw new Error(`Client invalide : ${texte}`);
}

// Sous strict, la variable d'un catch est unknown pour la meme raison : throw
// accepte n'importe quelle valeur, pas seulement une Error.
try {
  lireClient('{"nom":"Durand"}');
} catch (erreur) {
  // console.log(erreur.message);
  // error TS18046: 'erreur' is of type 'unknown'.
  console.log(erreur instanceof Error ? erreur.message : String(erreur));
  // Client invalide : {"nom":"Durand"}
}

À l'autre bout, never est le type qui n'a aucune valeur. Il est assignable à tout type et aucun type ne lui est assignable, sauf lui-même ; dans une union, il disparaît, string | never valant string. On le rencontre comme retour d'une fonction qui ne rend jamais la main, comme résidu d'un rétrécissement qui a tout éliminé, et parfois là où on ne l'attend pas : le tableau vide d'un littéral d'objet n'a aucun élément dont déduire un type.

// never est le type sans aucune valeur. Une fonction qui ne rend jamais la
// main, parce qu'elle leve toujours, le declare en retour.
function echouer(message: string): never {
  throw new Error(message);
}

function prixUnitaire(total: number, quantite: number): number {
  if (quantite > 0) {
    return total / quantite;
  }
  // echouer est une declaration de fonction : le compilateur tient la suite de
  // l'appel pour inatteignable, et aucun return ne manque.
  echouer('Quantite nulle');
}

// Deduit seul, le retour d'une fonction qui leve toujours depend de sa forme :
// never pour une flechee, void pour une declaration.
const leverFlechee = () => {
  throw new Error('jamais');
}; // () => never
function leverDeclaree() {
  throw new Error('jamais');
} // () => void

// Rendre never ne suffit pas : comme pour une assertion, l'appel doit viser une
// declaration de fonction ou un nom dont le type est ecrit. leverFlechee n'est
// ni l'un ni l'autre, et le flux de controle l'ignore.
// function prixOuErreur(quantite: number): number { if (quantite > 0) return 12; leverFlechee(); }
// error TS2366: Function lacks ending return statement and return type does not include 'undefined'.
const leverTypee: () => never = leverFlechee;

function prixOuErreurTypee(quantite: number): number {
  if (quantite > 0) return 12;
  leverTypee(); // accepte : le type de leverTypee est ecrit
}

// never apparait aussi la ou rien ne permet de deduire un type : le tableau
// vide d'un litteral d'objet.
const panier = { lignes: [] }; // { lignes: never[] }

// panier.lignes.push('USB-64');
// error TS2345: Argument of type '"USB-64"' is not assignable to parameter of type 'never'.

const panierType: { lignes: string[] } = { lignes: [] };
panierType.lignes.push('USB-64');

L'usage le plus rentable de never, le contrôle d'exhaustivité d'un switch, est développé dans le cours Types avancés.

Ce que le compilateur déduit seul

L'inférence opère dans trois directions. Une variable prend le type de son initialiseur. Une fonction dont le retour n'est pas annoté rend l'union de ce que rendent ses return, ce qui fait apparaître un undefined oublié plus sûrement qu'une annotation écrite de mémoire. Enfin, l'inférence contextuelle fait le chemin inverse : quand une fonction est écrite à un endroit où un type de fonction est attendu, ce type donne ceux de ses paramètres. C# connaît var pour les variables locales et le type cible pour les lambdas, mais exige le type de retour d'une méthode ; TypeScript le déduit aussi.

// Variables : le type vient de l'initialiseur.
let quantite = 3; // number
const lignes = [
  { reference: 'USB-64', prix: 12, quantite: 2 },
  { reference: 'SSD-1T', prix: 89, quantite: 1 },
]; // { reference: string; prix: number; quantite: number; }[]

// Un tableau heterogene recoit l'union de ses elements.
const valeurs = [12, 'USB-64', null]; // (string | number | null)[]

// Retours : le type rendu est deduit du corps, y compris ce qu'on oublie.
function trouver(reference: string) {
  return lignes.find((l) => l.reference === reference);
} // { reference: string; prix: number; quantite: number; } | undefined

function total() {
  return lignes.reduce((somme, l) => somme + l.prix * l.quantite, 0);
} // number

// console.log(trouver('SSD-1T').prix);
// error TS2532: Object is possibly 'undefined'.
console.log(trouver('SSD-1T')?.prix); // 89

// Contextuelle : le type attendu la ou la fonction est ecrite donne le type de
// ses parametres. l est une ligne, e un KeyboardEvent, sans annotation.
const libelles = lignes.map((l) => `${l.quantite} x ${l.reference}`); // string[]
document.addEventListener('keydown', (e) => console.log(e.key));

L'inférence contextuelle ne tient qu'à l'endroit où la fonction est écrite. Sortie de l'appel pour être nommée ou réutilisée, elle perd son contexte, et un paramètre sans type devient une erreur sous noImplicitAny. De même, une fonction récursive ne peut pas déduire un retour qui dépend de lui-même.

// Ecrite a l'endroit de l'appel, la fonction recoit son type du contexte.
document.addEventListener('keydown', (e) => console.log(e.key));

// Sortie dans une variable, elle n'a plus de type attendu : rien ne dit ce
// qu'est e, et noImplicitAny, compris dans strict, refuse d'en faire un any.
// const journaliser = (e) => console.log(e.key);
// error TS7006: Parameter 'e' implicitly has an 'any' type.

// Une fonction recursive depend de son propre type de retour.
// function factorielle(n: number) { return n <= 1 ? 1 : n * factorielle(n - 1); }
// error TS7023: 'factorielle' implicitly has return type 'any' because it does not have a return type annotation and is referenced directly or indirectly in one of its return expressions.

Dans les deux cas, une annotation suffit, et c'est le bon endroit pour en écrire une. La règle pratique : annoter les paramètres, les retours des fonctions exportées ou récursives, et laisser le reste à l'inférence.

// Le type de la variable devient le contexte de la fonction...
const journaliser: (e: KeyboardEvent) => void = (e) => console.log(e.key);
// ... ou le parametre porte lui-meme son type.
const journaliserAussi = (e: KeyboardEvent) => console.log(e.key);

document.addEventListener('keydown', journaliser);
document.addEventListener('keyup', journaliserAussi);

// Le retour annote rompt le cycle : l'appel recursif a un type connu.
function factorielle(n: number): number {
  return n <= 1 ? 1 : n * factorielle(n - 1);
}

console.log(factorielle(5)); // 120

Élargissement et as const

Un littéral a d'abord un type littéral : 'strict' est du type "strict", qui n'admet que cette chaîne. Quand la valeur peut changer, le compilateur l'élargit à string, parce qu'un type qui n'admet qu'une valeur interdirait toute réaffectation. Une variable const garde le littéral, une variable let l'élargit, et les propriétés d'un objet, modifiables même sous const, s'élargissent aussi. C'est là que l'élargissement surprend : un objet de configuration ou un résultat qui avait l'air précis ne l'est plus.

type Mode = 'strict' | 'souple';

function demarrer(mode: Mode, port: number): void {
  console.log(`${mode} sur ${port}`);
}

// const garde le litteral : la variable ne changera jamais.
const mode = 'strict'; // "strict"
// let l'elargit : la variable pourra recevoir une autre chaine.
let autreMode = 'strict'; // string

demarrer(mode, 4200);

// demarrer(autreMode, 4200);
// error TS2345: Argument of type 'string' is not assignable to parameter of type 'Mode'.

// Les proprietes d'un objet restent modifiables, meme sous const : elles
// s'elargissent comme des let.
const config = { mode: 'strict', port: 4200 }; // { mode: string; port: number; }

// demarrer(config.mode, config.port);
// error TS2345: Argument of type 'string' is not assignable to parameter of type 'Mode'.

// Meme effet sur un resultat : ok devient boolean dans les deux branches, et
// tester ok ne dit plus laquelle a ete prise.
function valider(saisie: string) {
  return saisie.length > 0 ? { ok: true, valeur: saisie } : { ok: false, erreur: 'vide' };
}

const resultat = valider('USB-64');
// { ok: boolean; valeur: string; erreur?: undefined; }
//   | { ok: boolean; erreur: string; valeur?: undefined; }

// if (resultat.ok) console.log(resultat.valeur.toUpperCase());
// error TS18048: 'resultat.valeur' is possibly 'undefined'.

as const demande l'inverse : aucun élargissement, et tout en readonly, en profondeur, un tableau devenant un tuple. Le readonly n'existe que dans les types ; contrairement à Object.freeze, il ne protège rien à l'exécution. Quand l'objet doit rester modifiable, une annotation fixe le type voulu sans figer les valeurs.

type Mode = 'strict' | 'souple';

function demarrer(mode: Mode, port: number): void {
  console.log(`${mode} sur ${port}`);
}

// as const : tout devient litteral et readonly, en profondeur.
const config = { mode: 'strict', port: 4200 } as const;
// { readonly mode: "strict"; readonly port: 4200; }
demarrer(config.mode, config.port); // strict sur 4200

// Une annotation, si l'objet doit rester modifiable.
const reglage: { mode: Mode; port: number } = { mode: 'strict', port: 4200 };
reglage.mode = 'souple';

// Le discriminant reste un litteral, et le test sur ok restreint le resultat.
function valider(saisie: string) {
  return saisie.length > 0
    ? { ok: true as const, valeur: saisie }
    : { ok: false as const, erreur: 'vide' };
}

const resultat = valider('usb-64');
if (resultat.ok) console.log(resultat.valeur.toUpperCase()); // USB-64

// Un tableau as const devient un tuple readonly : l'union s'en deduit sans
// recopier les valeurs.
const ROLES = ['admin', 'redacteur', 'lecteur'] as const;
type Role = (typeof ROLES)[number]; // "admin" | "redacteur" | "lecteur"

// ROLES.push('invite');
// error TS2339: Property 'push' does not exist on type 'readonly ["admin", "redacteur", "lecteur"]'.

Le rétrécissement

Dans une union, le compilateur suit le flux de contrôle : après un test, dans chaque branche, le type de la variable est restreint à ce que le test laisse possible. Le mécanisme s'appelle le rétrécissement (narrowing) et s'appuie sur les tests JavaScript ordinaires, ce qui fait sa force : aucune syntaxe à apprendre, et le code qui vérifie à l'exécution est le même qui informe le compilateur.

Par le flux de contrôle

typeof distingue les primitives, instanceof les classes, qui existent à l'exécution, et in les formes, par la présence d'une propriété. L'égalité et les tests de vérité restreignent aussi. Le piège classique est typeof null, qui vaut 'object' depuis la première version de JavaScript ; le compilateur le sait et laisse null dans la branche.

class ErreurMetier extends Error {
  constructor(public readonly code: string) {
    super(`Erreur metier ${code}`);
  }
}

interface Carte {
  numeroMasque: string;
}

interface Virement {
  iban: string;
}

function decrire(valeur: string | string[] | null): string {
  // typeof null vaut 'object' : ce test laisse passer null.
  // if (typeof valeur === 'object') console.log(valeur.length);
  // error TS18047: 'valeur' is possibly 'null'.

  // Egalite : null est ecarte, il reste string | string[].
  if (valeur === null) {
    return 'aucune';
  }
  // typeof : seul string[] repond 'object'.
  if (typeof valeur === 'object') {
    return valeur.join(', ');
  }
  return valeur; // string, le seul membre restant
}

function moyen(p: Carte | Virement): string {
  // in : la presence d'une propriete designe le membre de l'union.
  return 'iban' in p ? `Virement ${p.iban}` : `Carte ${p.numeroMasque}`;
}

function messageDe(erreur: unknown): string {
  // instanceof : une classe existe a l'execution, le test aussi.
  if (erreur instanceof ErreurMetier) {
    return `[${erreur.code}] ${erreur.message}`;
  }
  if (erreur instanceof Error) {
    return erreur.message;
  }
  return String(erreur);
}

console.log(decrire(['USB-64', 'SSD-1T'])); // USB-64, SSD-1T
console.log(moyen({ iban: 'FR76 3000' })); // Virement FR76 3000
console.log(messageDe(new ErreurMetier('STOCK'))); // [STOCK] Erreur metier STOCK

Une interface, elle, n'existe pas à l'exécution : instanceof Carte est refusé, et c'est in ou un discriminant qui prend le relais.

Gardes de type et assertions

Quand le test est trop long pour être répété, on le range dans une fonction dont le retour est un prédicat, valeur is Produit. L'appelant qui teste ce retour obtient le même rétrécissement que s'il avait écrit le test lui-même. Depuis TypeScript 5.5, le compilateur déduit ce prédicat seul pour une fonction qui n'a qu'un return, sans annotation de retour, dont le résultat est lié à un rétrécissement du paramètre.

interface Produit {
  reference: string;
  prix: number;
}

// Garde de type utilisateur : le retour "valeur is Produit" dit au compilateur
// ce qu'un true prouve.
function estProduit(valeur: unknown): valeur is Produit {
  return (
    typeof valeur === 'object' &&
    valeur !== null &&
    'reference' in valeur &&
    typeof valeur.reference === 'string' &&
    'prix' in valeur &&
    typeof valeur.prix === 'number'
  );
}

const recus: unknown[] = JSON.parse('[{"reference":"USB-64","prix":12},{"prix":"offert"}]');
const produits = recus.filter(estProduit); // Produit[]
console.log(produits.length); // 1

// Depuis TypeScript 5.5, une fleche qui fait un test simple devient elle-meme
// une garde, sans annotation.
const prix = [12, undefined, 89].filter((p) => p !== undefined); // number[]

// Pas de garde pour un test de verite : !!p est faux pour 0 aussi, un false
// ne prouverait donc pas que p vaut undefined.
const prixAussi = [12, undefined, 89].filter((p) => !!p); // (number | undefined)[]

// Le compilateur croit la garde sur parole : celle-ci compile, et ment.
function estProduitMenteur(valeur: unknown): valeur is Produit {
  return valeur !== null;
}

Le prédicat est une promesse que le compilateur ne vérifie pas : il contrôle que la fonction rend un booléen, pas que ce booléen dit vrai. Une garde écrite à la main mérite donc ses propres tests.

Une fonction d'assertion restreint sans if : son retour asserts condition ou asserts valeur is T signifie qu'elle lève si la condition est fausse, et qu'après l'appel la condition tient. Le compilateur impose que l'appel passe par une déclaration de fonction ou par un nom dont le type est écrit : sans cette règle, reconnaître une assertion l'obligerait à inférer le type d'une variable au milieu de l'analyse du flux, qui peut elle-même dépendre de ce type.

// asserts : la fonction ne rend rien, elle leve si la condition est fausse.
// Apres l'appel, le compilateur tient la condition pour acquise.
function affirmer(condition: unknown, message: string): asserts condition {
  if (!condition) throw new Error(message);
}

function affirmerDefini<T>(valeur: T, nom: string): asserts valeur is NonNullable<T> {
  if (valeur === null || valeur === undefined) throw new Error(`${nom} absent`);
}

function initialiser(saisie: string | number): void {
  const racine = document.getElementById('app'); // HTMLElement | null
  affirmerDefini(racine, '#app');
  racine.textContent = 'Pret'; // racine : HTMLElement

  affirmer(typeof saisie === 'string', 'reference attendue');
  console.log(saisie.toUpperCase()); // saisie : string
}

// L'appel d'une assertion doit passer par une declaration de fonction ou par
// un nom dont le type est ecrit : une flechee rangee dans un const sans
// annotation ne convient pas.
const affirmerChaine = (valeur: unknown): asserts valeur is string => {
  if (typeof valeur !== 'string') throw new Error('chaine attendue');
};

const affirmerTexte: (valeur: unknown) => asserts valeur is string = affirmerChaine;

function lire(valeur: unknown): string {
  // affirmerChaine(valeur);
  // error TS2775: Assertions require every name in the call target to be declared with an explicit type annotation.
  affirmerTexte(valeur);
  return valeur.trim();
}

Les propriétés en trop sur un littéral

Le typage structurel admet les propriétés en trop, sauf à un endroit : un littéral d'objet écrit directement là où un type est attendu. Un tel littéral, dit frais, n'a aucune autre utilisation possible ; une propriété que le type ne connaît pas y est donc presque sûrement une faute de frappe, et le compilateur la signale, avec une suggestion quand le nom est proche. La vérification disparaît dès que l'objet transite par une variable.

interface Options {
  delaiMs?: number;
  tentatives?: number;
}

function appeler(url: string, options: Options): void {
  console.log(url, options.delaiMs ?? 1000, options.tentatives ?? 1);
}

// Un litteral ecrit la ou un type est attendu ne peut pas porter de propriete
// inconnue : c'est presque toujours une faute de frappe.
// appeler('/api/produits', { delaiMS: 500, tentatives: 3 });
// error TS2561: Object literal may only specify known properties, but 'delaiMS' does not exist in type 'Options'. Did you mean to write 'delaiMs'?

// Le meme objet passe par une variable n'est plus un litteral frais : la regle
// structurelle reprend la main, et une propriete en trop est admise.
const reglages = { delaiMS: 500, tentatives: 3 };
appeler('/api/produits', reglages); // /api/produits 1000 3

// Reste une garde : un type dont toutes les proprietes sont optionnelles exige
// au moins une propriete en commun.
const fautif = { delaiMS: 500 };

// appeler('/api/produits', fautif);
// error TS2559: Type '{ delaiMS: number; }' has no properties in common with type 'Options'.

// La parade : typer l'objet la ou il est ecrit, et le controle a lieu la.
const reglagesTypes: Options = { delaiMs: 500, tentatives: 3 };
appeler('/api/produits', reglagesTypes); // /api/produits 500 3

Un type dont toutes les propriétés sont optionnelles, comme Options, garde une protection supplémentaire, la détection des types faibles : un objet qui n'a aucune propriété en commun avec lui est refusé, variable ou non. Enfin, as Options ferait taire le contrôle, car une assertion de type affirme au lieu de vérifier.

satisfies contre annotation

Une annotation fait deux choses : elle vérifie la valeur, puis elle remplace le type déduit par le type déclaré. Pour un objet de configuration, la seconde est souvent une perte. Une palette déclarée Record<Couleur, string | Rgb> oublie laquelle de ses entrées est une chaîne, et un Record<string, Route> accepte n'importe quelle clé, y compris celles qui n'existent pas.

type Couleur = 'rouge' | 'vert' | 'bleu';
type Rgb = [rouge: number, vert: number, bleu: number];

// L'annotation remplace le type deduit par le type declare : chaque valeur
// n'est plus que string | Rgb, quelle qu'ait ete sa forme.
const palette: Record<Couleur, string | Rgb> = {
  rouge: [255, 0, 0],
  vert: '#00ff00',
  bleu: [0, 0, 255],
};

// palette.vert.toUpperCase();
// error TS2339: Property 'toUpperCase' does not exist on type 'string | Rgb'.
//   Property 'toUpperCase' does not exist on type 'Rgb'.

interface Route {
  chemin: string;
  titre?: string;
}

// Avec des cles string, toute cle est admise, meme celles qui n'existent pas
// (strict n'active pas noUncheckedIndexedAccess, qui ferait signaler cet acces).
const routes: Record<string, Route> = {
  accueil: { chemin: '/' },
  catalogue: { chemin: '/catalogue', titre: 'Catalogue' },
};

console.log(routes['catalgue'].chemin);
// A l'execution (Node, Chrome) : TypeError: Cannot read properties of undefined (reading 'chemin')

L'opérateur satisfies ne garde que la première. La valeur est vérifiée contre le type — clés manquantes, propriétés en trop, types des valeurs — puis conserve son type déduit. Le type vérifié sert aussi de contexte : c'est lui qui fait déduire un tuple [number, number, number] là où le littéral seul aurait donné number[]. Combiné à as const, il fige les valeurs et contrôle la forme en une expression.

type Couleur = 'rouge' | 'vert' | 'bleu';
type Rgb = [rouge: number, vert: number, bleu: number];

// satisfies verifie la conformite, puis laisse le type deduit en place.
const palette = {
  rouge: [255, 0, 0],
  vert: '#00ff00',
  bleu: [0, 0, 255],
} satisfies Record<Couleur, string | Rgb>;
// { rouge: [number, number, number]; vert: string; bleu: [number, number, number]; }

console.log(palette.vert.toUpperCase()); // #00FF00
const [r, v, b] = palette.rouge;

// Le controle reste celui d'une annotation : cle manquante, cle en trop.
// const incomplete = { rouge: '#f00', vert: '#0f0' } satisfies Record<Couleur, string>;
// error TS1360: Type '{ rouge: string; vert: string; }' does not satisfy the expected type 'Record<Couleur, string>'.
//   Property 'bleu' is missing in type '{ rouge: string; vert: string; }' but required in type 'Record<Couleur, string>'.

// const fautive = { rouge: '#f00', vert: '#0f0', blue: '#00f' } satisfies Record<Couleur, string>;
// error TS2353: Object literal may only specify known properties, and 'blue' does not exist in type 'Record<Couleur, string>'.

interface Route {
  chemin: string;
  titre?: string;
}

// Les cles restent celles du litteral : une faute de frappe est une erreur.
const routes = {
  accueil: { chemin: '/' },
  catalogue: { chemin: '/catalogue', titre: 'Catalogue' },
} satisfies Record<string, Route>;

// console.log(routes.catalgue.chemin);
// error TS2551: Property 'catalgue' does not exist on type '{ accueil: { chemin: string; }; catalogue: { chemin: string; titre: string; }; }'. Did you mean 'catalogue'?

// Avec as const, les valeurs restent litterales et readonly, et la forme verifiee.
const CHEMINS = { accueil: '/', catalogue: '/catalogue' } as const satisfies Record<string, string>;
// { readonly accueil: "/"; readonly catalogue: "/catalogue"; }

Le choix se fait sur ce que le code attend ensuite. Une variable qui doit accepter n'importe quelle valeur du type déclaré, parce qu'elle sera réaffectée ou exposée comme contrat, prend une annotation. Une constante dont on veut garder le détail — les clés d'un dictionnaire, les littéraux d'une table — prend satisfies.

Ce cours vous a servi ? Offrir un café Signaler une erreur