Lange Zeit musstest du dich in TypeScript zwischen zwei Übeln entscheiden: Eine Type Annotation wie : Record<string, string> prüft zwar die Form deines Werts, weitet aber gleichzeitig dessen Inferenz-Typ auf den deklarierten Typ aus — der schmale Literal-Typ ist verloren. Eine Type Assertion mit as hält den Wert schmal, schaltet aber den prüfenden Compiler praktisch aus und kann hemmungslos lügen. Mit TypeScript 4.9 kam der satisfies-Operator, der genau diese Lücke schließt: Er prüft, dass dein Ausdruck einem Constraint genügt, ohne den inferierten Typ zu verändern. Das Ergebnis ist eine validierte, aber so präzise wie möglich getypte Konstante — ideal für Konfigurations-Objekte, Routen-Tabellen, Handler-Maps und alle Strukturen mit „bekannten Keys". satisfies ist kein Ersatz für Annotationen oder Assertions, sondern eine eigenständige, oft überlegene Alternative. Wer den Operator einmal verinnerlicht hat, greift bei Object-Literalen kaum noch zu as.
Das Problem vor satisfies
Stell dir eine Routen-Konfiguration vor: Du willst sicherstellen, dass alle Werte URLs sind (string), aber später beim Zugriff den exakten Literal-Wert zur Verfügung haben. Mit den klassischen Mitteln musst du dich zwischen Sicherheit und Präzision entscheiden.
// Variante A — Type Annotation: prüft, aber widet
const routesA: Record<string, string> = {
home: "/",
about: "/about",
contact: "/contact",
};
routesA.home;
// Typ: string — der Literal "/" ist verloren.
// Variante B — Type Assertion: schmal, aber ungeprüft
const routesB = {
home: "/",
about: "/about",
contact: "/contact",
} as Record<string, string>;
// Lügt potenziell — wäre auch akzeptiert worden, wenn
// ein Wert numerisch oder null wäre.
// Variante C — gar nichts: schmal, aber kein Constraint
const routesC = {
home: "/",
about: 42, // niemand stoppt dich.
contact: "/contact",
};Keine der drei Varianten liefert „beides gleichzeitig": Form-Validierung und Erhalt der Literal-Typen. Genau das ist die Lücke, die satisfies füllt.
Wie satisfies funktioniert
Der Operator steht als Postfix hinter einem Ausdruck und nennt einen Constraint-Typ. Der Compiler prüft, ob der inferierte Typ des Ausdrucks dem Constraint zuweisbar ist — und behält danach den schmalen Inferenz-Typ als Typ der Variable.
const value = expression satisfies ConstraintType;
// ^^^^^^^^^^^^^^^^^^^^^^^^
// Compile-Zeit-Check ohne Typ-Änderung.Drei Regeln zur Erinnerung:
satisfiesändert den Inferenz-Typ nicht — er bleibt so schmal wie ohne den Operator.satisfiesist rein statisch — es entsteht kein Laufzeit-Code, vergleichbar mitas.satisfiesverbietet fehlende Pflicht-Keys und falsche Werte, lässt aber überschüssige Properties in der gleichen Strenge wie eine Annotation auffliegen (Excess-Property-Check auf Object-Literalen).
Erstes Beispiel: Color-Palette
Das klassische Lehrbuch-Beispiel aus den 4.9-Release-Notes — leicht gekürzt:
const palette = {
red: "#f00",
green: "#0f0",
blue: "#00f",
} satisfies Record<string, string>;
palette.red;
// Typ: "#f00" — nicht string!
// Der Compiler hat geprüft, dass alle Werte Strings sind,
// behält aber den Literal-Typ jedes einzelnen Eintrags.Der Unterschied zur Annotation wird beim Aufruf sichtbar — übergibst du palette.red an eine Funktion mit Parameter-Typ "#f00" | "#0f0" | "#00f", geht das mit satisfies problemlos, mit Annotation nicht. Auch Tippfehler werden gefangen:
const palette = {
red: "#f00",
green: "#0f0",
bleu: "#00f", // OK für Record<string, string>!
} satisfies Record<string, string>;Hier hilft Record<string, string> nicht — jeder Key-Tippfehler bleibt unbemerkt. Die Stärke kommt erst zum Tragen, wenn du den Key-Typ einschränkst: satisfies Record<"red" | "green" | "blue", string> würde bleu als Fehler ausweisen.
Annotation vs. as vs. satisfies
Die drei Mechanismen lassen sich am gleichen Beispiel direkt vergleichen — gleiche Daten, drei Strategien.
// 1) Annotation — prüft, widet, verliert Literal-Typen
const a: Record<"x" | "y", number> = { x: 1, y: 2 };
a.x; // number
// 2) as — lügt potenziell, behält aber den Inferenz-Typ nur,
// wenn der Ziel-Typ schmal genug ist.
const b = { x: 1, y: 2 } as Record<"x" | "y", number>;
b.x; // number — auch hier verbreitert.
// 3) satisfies — prüft, behält den Inferenz-Typ
const c = { x: 1, y: 2 } satisfies Record<"x" | "y", number>;
c.x; // 1 (Literal!)Im direkten Tabellen-Überblick:
| Mechanismus | Form-Validierung | Inferenz-Typ | Risiko |
|---|---|---|---|
: T (Annotation) | + voll | − wird zu T geweitet | klein — Compiler prüft jede Property |
as T (Assertion) | − keine echte Prüfung | − wird zu T umgedeutet | hoch — kann lügen, Cast in beide Richtungen erlaubt |
satisfies T | + voll | + bleibt schmal | klein — wie Annotation, aber ohne Widening |
Faustregel: Wenn du den Inferenz-Typ später noch brauchst, nutze satisfies. Eine Annotation ist dann sinnvoll, wenn du den Typ tatsächlich auf T festlegen willst (etwa als öffentlich exportiertes Symbol mit explizitem Vertrag). as bleibt für Fälle, in denen der Compiler nicht genug Informationen hat — etwa beim Aufbau eines Werts in mehreren Schritten oder bei der Interaktion mit nicht-getypten externen Daten.
Praxis: Form-Schema-Validierung
Validatoren-Maps sind ein klassischer Anwendungsfall. Du hast ein Formular mit bekannten Feldern, und für jedes Feld einen Validator. Die Map soll zwei Eigenschaften haben: Alle Felder müssen abgedeckt sein, und beim Zugriff soll der Compiler die exakte Validator-Signatur des konkreten Felds kennen.
type FormField = "email" | "password" | "age";
type Validator<T> = (value: T) => string | null;
const validators = {
email: (v: string) => v.includes("@") ? null : "Ungültige E-Mail",
password: (v: string) => v.length >= 8 ? null : "Mindestens 8 Zeichen",
age: (v: number) => v >= 18 ? null : "Mindestens 18",
} satisfies Record<FormField, Validator<any>>;
validators.email;
// Typ: (v: string) => string | null — exakte Signatur.
validators.age(17);
// OK — Parameter ist als number bekannt.
// Ohne satisfies wäre der Parameter-Typ auf any geweitet.Mit Record<FormField, Validator<any>> als Annotation würden alle Validator-Funktionen den Parameter-Typ any annehmen — die schmalen Signaturen wären verloren. satisfies prüft die Constraint („jedes Feld hat einen Validator"), behält aber pro Eintrag die schmal-inferierte Funktion mit ihrem konkreten Parameter-Typ.
Praxis: API-Response-Mappings
Status-Codes zu Handlern — eine Map, in der jeder Status seinen eigenen Handler mit eigenem Datenformat hat. Hier kommt die Stärke von satisfies voll zur Geltung.
type Status = 200 | 400 | 401 | 404 | 500;
const handlers = {
200: (data: { items: string[] }) => data.items,
400: (err: { message: string }) => `Bad Request: ${err.message}`,
401: () => "Bitte einloggen",
404: () => "Nicht gefunden",
500: (err: { stack: string }) => `Server-Fehler: ${err.stack}`,
} satisfies Record<Status, (...args: any[]) => unknown>;
const result = handlers[200]({ items: ["a", "b"] });
// Typ: string[] — exakter Rückgabe-Typ pro Status.
handlers[401]();
// Typ: string — keine Argumente, kein any.Der Constraint Record<Status, (...args: any[]) => unknown> zwingt nur dazu, dass für jeden Status ein Callable existiert. Den konkreten Signatur-Typ jedes Handlers — Parameter wie Rückgabe — liest TypeScript aus dem Object-Literal selbst aus. Beim Aufruf von handlers[200] weiß der Compiler, dass das erste Argument ein { items: string[] } sein muss, nicht ein beliebiger any.
Kombination mit as const
as const und satisfies schließen sich nicht aus — im Gegenteil, sie ergänzen sich. Mit as const werden Primitiv-Werte zu Literal-Typen und alle Properties readonly. Mit satisfies danach prüfst du, dass das Ergebnis einem Constraint genügt — ohne dabei die volle Literal-Verengung zu verlieren.
type ColorToken = "background" | "surface" | "primary" | "text";
const tokens = {
background: "#fff",
surface: "#f8f8f8",
primary: "#0066ff",
text: "#111",
} as const satisfies Record<ColorToken, string>;
tokens.primary;
// Typ: "#0066ff" (readonly Literal).
type Token = typeof tokens[keyof typeof tokens];
// "#fff" | "#f8f8f8" | "#0066ff" | "#111"Drei Effekte gleichzeitig:
as constmacht jeden Wert zum Literal-Typ und friert die Struktur ein.satisfies Record<ColorToken, string>prüft, dass alle Tokens vorhanden und alle Werte Strings sind.- Der inferierte Typ bleibt die schmale
readonly-Struktur mit Literal-Werten — perfekt fürtypeof tokens[keyof typeof tokens]-Extraktion.
Die Reihenfolge ist wichtig: as const kommt vor satisfies. Erst die Verengung, dann der Check.
Wann satisfies, wann nicht?
satisfies glänzt überall dort, wo der Compiler den schmalen Inferenz-Typ und eine Form-Validierung kombinieren soll. Drei Daumenregeln:
- Konfigurations-Objekte mit bekannten Keys: Routen-Tabellen, Theme-Tokens, Handler-Maps, Validator-Maps — überall, wo Keys aus einem Literal-Union stammen und Werte pro Key unterschiedliche Sub-Typen haben dürfen.
- Funktions-Returns mit Literal-Genauigkeit: Wenn eine Factory-Funktion ein Objekt zurückgibt, dessen Werte später als Literale referenziert werden sollen, ersetzt
satisfiesam Return-Statement die explizite Return-Annotation. - Tagged-Constants-Maps: Discriminant-Properties mit Literal-Typen — die Diskriminierung in
switch-Blöcken funktioniert nur, wenn die Discriminant-Property als Literal-Typ erhalten bleibt.
Wo satisfies nicht das richtige Werkzeug ist:
- Externe, nicht-getypte Daten — etwa JSON aus einer API. Hier brauchst du Runtime-Validierung (Zod, Valibot, io-ts) und danach eine echte Annotation.
satisfiesist Compile-Time-only und schützt nicht vor Daten, die der Compiler nie gesehen hat. - Öffentliche, exportierte Verträge — wenn du explizit den Typ
Record<string, string>als Schnittstelle nach außen geben willst, ist die Annotation die ehrlichere Wahl.satisfieslässt den Inferenz-Typ schmal, was bei öffentlichen APIs zu unerwarteter Verengung führen kann. - Vorgefertigte Werte ohne Inferenz-Bedarf — wenn der Wert einmalig irgendwohin übergeben wird und der konkrete Typ nicht mehr referenziert wird, reicht eine Annotation.
Besonderheiten
satisfies wurde mit TypeScript 4.9 (November 2022) eingeführt.
Vor 4.9 gab es nur Workarounds: Hilfsfunktionen wie function asTyped<T>(), die als Identitäts-Funktion einen Constraint prüften und den schmalen Typ zurückgaben. Diese Helper sind heute überflüssig — satisfies erledigt dasselbe nativ und ohne Funktions-Indirektion. Wer ältere TypeScript-Projekte übernimmt, findet oft noch solche Helper unter Namen wie tuple(), asConst(), typed().
satisfies und as schließen sich nicht aus.
Das idiomatische Pattern für Konfigurations-Konstanten lautet ... as const satisfies T. as const verengt jeden Wert auf seinen Literal-Typ und friert die Struktur ein; satisfies T prüft danach, dass die so entstandene Form dem Constraint genügt. Die Reihenfolge ist entscheidend: erst as const, dann satisfies. Umgekehrt würde der as const-Effekt erst nach der Prüfung greifen und die Literal-Inferenz wäre kaputt.
Style-Guides empfehlen satisfies vor as bei Object-Literalen.
Viele moderne TypeScript-Style-Guides — darunter Effective TypeScript und mehrere Frontend-Frameworks — bevorzugen satisfies gegenüber as überall dort, wo es technisch möglich ist. Begründung: as kann lügen, satisfies nicht. Wer in einem Code-Review eine as-Assertion auf einem Object-Literal sieht, sollte prüfen, ob satisfies denselben Job sicherer macht.
satisfies ändert den Inferenz-Typ NICHT.
Das ist der Kern-Unterschied zu as und zur Annotation. Eine Variable, die mit satisfies T deklariert wird, hat als statischen Typ den schmalen Inferenz-Typ des Ausdrucks, nicht T. T ist nur die Constraint, gegen die geprüft wird. Konsequenz: Beim Hover in der IDE siehst du den schmalen Typ, nicht den Constraint — was anfangs verwirrt, aber genau die gewollte Präzision liefert.
Bei Funktions-Returns kann satisfies die Annotation ersetzen.
Statt function makeConfig(): Record<Key, Value> kannst du function makeConfig() { return {...} satisfies Record<Key, Value>; } schreiben. Der Vorteil: Der Aufrufer bekommt den schmalen Inferenz-Typ, nicht den breiten Constraint. Nachteil: Die Funktions-Signatur ist nicht mehr direkt aus der Deklaration ablesbar — der Return-Typ muss aus dem Body inferiert werden, was bei öffentlichen APIs gegen den expliziten-Typ-Stil verstößt.
satisfies vs. interface-Implementierung: ähnliche Intention, andere Stelle.
class Foo implements Bar sagt dem Compiler: prüfe, dass Foo alle Pflicht-Member von Bar hat — aber der Typ von Foo bleibt Foo, nicht Bar. Genau das macht satisfies für Werte. Der Operator ist konzeptionell ein implements für Objekt-Literale, Funktionen und Konstanten.
Häufiges Pattern: Konfigurations-Objekte mit "bekannten Keys".
Routen, Theme-Tokens, Status-Handler, Form-Validatoren, i18n-Maps, Feature-Flag-Listen — sobald du ein Object-Literal mit Keys aus einer Literal-Union schreibst, ist satisfies mit hoher Wahrscheinlichkeit das richtige Werkzeug. Faustregel: Wenn du dachtest „hier brauche ich as const", brauchst du oft auch noch satisfies dahinter, um die Vollständigkeit der Keys zu prüfen.
satisfies eignet sich auch für Tagged-Constants-Maps.
Discriminated Unions mit Konstanten-Maps sind ein Power-Pattern: const Events = { click: { type: "click", ... }, hover: { type: "hover", ... } } as const satisfies Record<string, { type: string }>. Das Resultat ist eine Map von Tag-Konstanten, deren type-Property als Literal-Typ erhalten bleibt — perfekt für Switch-Diskriminierung beim Konsumenten.
Weiterführende Ressourcen
Externe Quellen
- TypeScript 4.9 Release Notes (satisfies)
- Type System – TypeScript Handbook
- satisfies – Playground Examples