Le mot-clé satisfies en TypeScript : à quoi ça sert concrètement

Le mot-clé satisfies est arrivé dans TypeScript 4.9, le 15 novembre 2022, et on peut très bien le croiser dans du code sans voir quand s'en servir. L'annonce officielle de Microsoft le résume en une phrase : il permet de vérifier que le type d'une expression correspond à un type donné, sans changer le type obtenu pour cette expression. Dit comme ça, c'est abstrait. Vu sur un objet de configuration, c'est très concret : c'est la réponse à un dilemme que vous avez sans doute déjà rencontré.

Tout ce qui suit a été compilé avec TypeScript 7.0.2 (la version latest du registre npm le 25 septembre 2026) et Node 20.19.4, en mode --strict. Les messages d'erreur sont recopiés tels que le compilateur les affiche, en anglais, avec leur ligne et leur colonne.

Le dilemme : vérifier un objet ou garder son type précis

Prenons une configuration de serveur de développement. Chaque réglage peut être un texte, un nombre ou un booléen, et on veut que TypeScript refuse tout le reste. Le réflexe est d'écrire un type et de l'annoncer sur la variable, avec les deux-points :

type Valeur = string | number | boolean;
type Config = Record<string, Valeur>;

const config: Config = {
  hote: "localhost",
  port: 5173,
  https: false,
};

console.log(config.port.toFixed(0));
console.log(config.prot);

La compilation échoue, mais pas là où on l'attendait :

src/1-annotation.ts(10,25): error TS2339: Property 'toFixed' does not exist on type 'Valeur'.
  Property 'toFixed' does not exist on type 'string'.

Deux problèmes en un seul essai. D'abord, config.port n'est plus un nombre aux yeux de TypeScript : l'annotation a remplacé ce qu'il savait de l'objet par ce que dit le type Config, où chaque réglage peut être un texte, un nombre ou un booléen. Il refuse donc toFixed, alors que vous avez écrit 5173 quelques lignes plus haut. Ensuite, et c'est plus grave, la ligne 11 ne produit aucune erreur. config.prot est une faute de frappe, mais Record<string, Valeur> accepte n'importe quelle clé : le compilateur n'a aucun moyen de savoir que prot n'existe pas.

Ne rien écrire : tout est précis, rien n'est vérifié

L'autre option consiste à ne pas annoncer de type du tout et à laisser TypeScript déduire celui de l'objet. Ajoutons au passage une valeur que la configuration n'aurait jamais dû accepter, debug: null :

const config = {
  hote: "localhost",
  port: 5173,
  https: false,
  debug: null,
};

console.log(config.port.toFixed(0));

Cette fois le compilateur ne dit rien du tout, code de sortie 0. Le type qu'il a retenu se lit dans le fichier de déclaration qu'il produit avec l'option --declaration :

declare const config: {
    hote: string;
    port: number;
    https: boolean;
    debug: null;
};

port est bien un nombre, toFixed passe. Mais personne n'a vérifié, là où l'objet est écrit, qu'il respecte la règle qu'on s'était donnée : le null est entré sans un mot. Le compilateur ne le signalera que plus loin, à l'endroit où une fonction qui attend une Valeur recevra config.debug, avec l'erreur TS2345: Argument of type 'null' is not assignable to parameter of type 'Valeur'., et sur la ligne de l'appel, pas sur celle de la faute. On a gagné la précision, on a perdu la vérification à la source.

satisfies : la vérification sans la perte

satisfies se place après la valeur, et non sur la variable. Il demande à TypeScript de vérifier que l'objet respecte Config, puis de garder le type qu'il a déduit de l'objet. Reprenons l'exemple fautif, avec le null et la faute de frappe :

type Valeur = string | number | boolean;
type Config = Record<string, Valeur>;

const config = {
  hote: "localhost",
  port: 5173,
  https: false,
  debug: null,
} satisfies Config;

console.log(config.port.toFixed(0));
console.log(config.prot);
src/3-satisfies.ts(8,3): error TS2322: Type 'null' is not assignable to type 'Valeur'.
src/3-satisfies.ts(12,20): error TS2339: Property 'prot' does not exist on type '{ hote: string; port: number; https: false; debug: null; }'.

Les deux erreurs qui nous avaient échappé sont signalées, chacune sur sa ligne : le null est refusé parce qu'il ne fait pas partie de Valeur, et prot est refusé parce que le type retenu est celui de l'objet, qui ne connaît que ses quatre clés. En revanche, la ligne 11 n'apparaît pas : config.port.toFixed(0) est accepté, parce que port est resté un nombre.

Une fois le null et la faute retirés, le fichier compile et le type retenu est bien le détail de l'objet, pas Config. Voici le code corrigé, puis la partie utile de la déclaration produite :

type Valeur = string | number | boolean;
type Config = Record<string, Valeur>;

const config = {
  hote: "localhost",
  port: 5173,
  https: false,
} satisfies Config;

console.log(config.port.toFixed(0));
console.log(config.hote.toUpperCase());
declare const config: {
    hote: string;
    port: number;
    https: false;
};

Remarquez https: false et non boolean : on y reviendra, c'est un piège de satisfies à connaître. Et à l'exécution ? Rien. satisfies n'existe que pour le compilateur, qui l'efface. Voici le JavaScript émis pour ce fichier, où il ne reste aucune trace du mot-clé :

"use strict";
const config = {
    hote: "localhost",
    port: 5173,
    https: false,
};
console.log(config.port.toFixed(0));
console.log(config.hote.toUpperCase());

Exécuté avec Node, il affiche 5173 puis LOCALHOST. Si vous découvrez tout juste cette frontière entre ce que vérifie le compilateur et ce qui tourne réellement, le guide sur Zod montre ce qu'elle implique pour les données qui arrivent de l'extérieur : satisfies vérifie ce que vous écrivez dans le code, jamais ce qu'une API vous renvoie.

: Configsatisfies Configas Configobjet vérifié ?objet vérifié ?objet vérifié ?type retenutype retenutype retenuouiouipresque pasConfigcelui de l'objetConfigprécision perdueprécision gardéeerreurs cachéesSeul satisfies vérifie sans effacer le détail.
Les trois façons de relier un objet à un type : l'annotation vérifie puis efface le détail, satisfies vérifie et garde le détail, as ne vérifie presque rien.

Ce que satisfies attrape, et que as laisse passer

La troisième façon de relier un objet à un type, c'est l'assertion as. Elle a l'air voisine, elle fait l'inverse : au lieu de demander au compilateur de vérifier, elle lui demande de vous croire. Voici une configuration à laquelle il manque le port :

type ConfigServeur = { hote: string; port: number };

const config = { hote: "localhost" } as ConfigServeur;

console.log(config.port.toFixed(0));

Le fichier compile sans une erreur, code de sortie 0. Le JavaScript produit, lui, s'arrête à la première exécution :

TypeError: Cannot read properties of undefined (reading 'toFixed')

Le même objet avec satisfies ne va pas si loin, le compilateur refuse dès la déclaration :

type ConfigServeur = { hote: string; port: number };

const config = { hote: "localhost" } satisfies ConfigServeur;
src/7-as-vs-satisfies.ts(3,38): error TS2741: Property 'port' is missing in type '{ hote: string; }' but required in type 'ConfigServeur'.

Pour être exact, as n'accepte pas absolument tout : il refuse une conversion entre deux types trop peu compatibles. const port = "8080" as number; produit l'erreur TS2352, dont le message conseille de passer par unknown si c'était voulu, et le même objet de configuration avec port: "5173" en texte est refusé de la même façon, malgré sa propriété hote correcte. Mais entre un objet auquel il manque simplement une propriété et le type complet, il y a assez de recouvrement pour qu'il vous laisse faire. C'est pour cela que le schéma parle de « presque pas » : as sert à dire au compilateur quelque chose qu'il ne peut pas savoir, pas à vérifier ce que vous avez écrit.

La même vérification marche avec des clés fixées à l'avance. Un objet qui doit fournir une URL pour chaque environnement se décrit avec Record et une union de noms, et un oubli est signalé :

type Environnement = "dev" | "prod";

const urls = {
  dev: "http://localhost:5173",
} satisfies Record<Environnement, string>;
src/5-cles-fixes.ts(5,3): error TS2741: Property 'prod' is missing in type '{ dev: string; }' but required in type 'Record<Environnement, string>'.

as const satisfies : des routes vérifiées et exactes

Voici une application très concrète. On veut une liste de routes d'un site, dont chacune doit commencer par une barre oblique, et on veut aussi que TypeScript connaisse la valeur exacte de chaque route. Le type `/${string}` exprime la première règle (un texte qui commence par /), as const fige les valeurs, et satisfies vérifie l'ensemble :

export const routes = {
  accueil: "/",
  articles: "/articles/",
  contact: "contact/",
} as const satisfies Record<string, `/${string}`>;
src/8-routes.ts(4,3): error TS2322: Type '"contact/"' is not assignable to type '`/${string}`'.

La barre oblique manquante est trouvée, sur la bonne ligne. Une fois corrigée, voici ce que TypeScript retient, en ajoutant une ligne qui récupère toutes les valeurs :

export const routes = {
  accueil: "/",
  articles: "/articles/",
  contact: "/contact/",
} as const satisfies Record<string, `/${string}`>;

export const toutes = Object.values(routes);
export declare const routes: {
    readonly accueil: "/";
    readonly articles: "/articles/";
    readonly contact: "/contact/";
};
export declare const toutes: ("/" | "/articles/" | "/contact/")[];

Chaque route est connue à la lettre près, et toutes est un tableau de ces trois valeurs exactement. Le même objet annoté avec les deux-points, const routes: Record<string, `/${string}`> = { ... }, compile aussi, mais la déclaration produite n'en garde rien :

export declare const routes: Record<string, `/${string}`>;
export declare const toutes: `/${string}`[];

Avec l'annotation, impossible d'en tirer le type des routes du site : le mieux qu'on obtienne est « n'importe quel texte qui commence par une barre oblique », et une fonction typée ainsi accepterait "/blog/", qui n'existe pas. Avec as const satisfies, elle n'accepte que ces trois-là. L'ordre compte : as const d'abord, pour figer les valeurs, satisfies ensuite, pour vérifier le résultat figé. Cette façon de faire travailler l'inférence à votre place rejoint l'inférence décrite dans l'article sur les génériques, où le compilateur déduit le type de la valeur qu'on lui passe au lieu de vous le faire écrire.

Le piège : une valeur qu'on voulait pouvoir modifier

Revenons au https: false de tout à l'heure. Quand le type vérifié contient des valeurs exactes, comme true et false dans boolean, ou "dev" et "prod" dans une union, satisfies garde la valeur exacte au lieu de l'élargir. Tant qu'on ne fait que lire l'objet, c'est un avantage. Dès qu'on veut le modifier, ça coince :

type Valeur = string | number | boolean;
type Config = Record<string, Valeur>;

const config = {
  hote: "localhost",
  port: 5173,
  https: false,
} satisfies Config;

config.port = 8080;
config.https = true;
src/12-mutation.ts(11,1): error TS2322: Type 'true' is not assignable to type 'false'.

La ligne 10 passe, parce que port a été élargi en number. La ligne 11 est refusée : pour TypeScript, https vaut false pour toujours. Déclarer la variable avec let n'y change rien : let autorise à remplacer l'objet entier, mais le type retenu reste celui du départ, et le nouvel objet doit s'y conformer :

type Mode = "dev" | "prod";

let reglage = { mode: "dev" } satisfies { mode: Mode };
reglage = { mode: "prod" };
src/10-let.ts(4,13): error TS2322: Type '"prod"' is not assignable to type '"dev"'.

Même cause, autre symptôme : une propriété facultative du type vérifié, absente de l'objet, n'existe pas dans le type retenu. Avec type Options = { hote: string; port?: number }, l'objet { hote: "localhost" } satisfies Options passe la vérification, mais la ligne options.port = 3000; qui suit est refusée avec l'erreur TS2339: Property 'port' does not exist on type '{ hote: string; }'. Le type vérifié sert de contrôle, jamais de type à la variable.

La correction n'a rien de mystérieux : un objet destiné à changer doit porter le type qui décrit tous ses états possibles, donc une annotation. Avec let reglage: Reglage = { mode: "dev" };, où Reglage vaut { mode: Mode }, la ligne reglage = { mode: "prod" }; compile et le programme affiche prod.

La règle que j'applique

Après ces mesures, ma règle tient en trois lignes :

Un dernier point pratique : satisfies exige TypeScript 4.9 au minimum. Le même fichier compilé avec TypeScript 4.8.4 échoue sur deux erreurs TS1005: ',' expected., la version 4.8 ne connaissant pas le mot-clé ; avec 4.9.3 il compile. Si un projet ancien le refuse, npx tsc -v affiche la version réellement utilisée, et c'est la première chose à vérifier. Pour ajouter TypeScript à un projet qui n'en a pas encore, le guide de migration de JavaScript vers TypeScript part de l'installation et va jusqu'au code compilé.