TypeScript satisfies: Typsicherheit ohne Inferenzverlust
Der satisfies-Operator prüft, ob ein Wert einem Typ entspricht, ohne die Literal-Inferenz zu verlieren — warum das zählt und wann du ihn nutzt.

Der satisfies-Operator kam mit TypeScript 4.9 und gehört bis heute zu den am wenigsten genutzten Funktionen der Sprache. In den meisten Codebasen, die davon profitieren könnten, greift man stattdessen zu Typannotationen – die den Typ aufweiten – oder zu as-Assertions – die den Compiler anlügen. satisfies löst dieses Dilemma: Es prüft, ob ein Wert einem Typ entspricht, ohne zu verändern, was der Compiler über die konkrete Form dieses Werts weiß.
Wer den Unterschied zwischen Validierung und Aufweitung versteht, erschließt sich eine ganze Klasse von Mustern, die Konfigurationsobjekte, Lookup-Tabellen und Registries wirklich sicherer und ausdrucksstärker machen.
Das Aufweitungsproblem bei Typannotationen
Wenn du eine Variable annotierst, nutzt TypeScript diese Annotation als Quelle der Wahrheit. Jede Information, die enger ist als die Annotation, geht dabei unwiderruflich verloren.
// ❌ Annotation widens — literal types are gone
const config: Record<string, { method: "GET" | "POST" | "PUT" | "DELETE" }> = {
getUser: { method: "GET" },
createUser: { method: "POST" },
};
// config.getUser.method is "GET" | "POST" | "PUT" | "DELETE"
// You can't use it where only "GET" is accepted without another assertion
// ✅ satisfies validates the shape and preserves literals
const config = {
getUser: { method: "GET" },
createUser: { method: "POST" },
} satisfies Record<string, { method: "GET" | "POST" | "PUT" | "DELETE" }>;
// config.getUser.method is "GET" — the literal is preserved
type GetMethod = typeof config.getUser.method; // "GET"Dieselbe strukturelle Validierung. Reichhaltigere Typen im weiteren Verlauf. Das ist der zentrale Kompromiss.
Wo das in der Praxis wichtig wird
Route-Registries sind das klassische Beispiel. Eine Codebasis, die typisierte API-Clients generiert oder Routing-Tabellen für Berechtigungsprüfungen nutzt, braucht mehr als reine Formvalidierung – die literalen Werte müssen erhalten bleiben.
type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
interface RouteDefinition {
path: string;
method: HttpMethod;
auth: boolean;
}
// ❌ After this annotation, specificity is gone
const ROUTES: Record<string, RouteDefinition> = {
listUsers: { path: "/users", method: "GET", auth: true },
createUser: { path: "/users", method: "POST", auth: true },
deleteUser: { path: "/users/:id", method: "DELETE", auth: true },
};
// ROUTES.listUsers.method is HttpMethod — you've lost "GET"
// Every consumer has to assert or runtime-check method values
// ✅ Shape validated, literals preserved
const ROUTES = {
listUsers: { path: "/users", method: "GET", auth: true },
createUser: { path: "/users", method: "POST", auth: true },
deleteUser: { path: "/users/:id", method: "DELETE", auth: true },
} satisfies Record<string, RouteDefinition>;
// ROUTES.listUsers.method is "GET"
// ROUTES.createUser.method is "POST"
type ListMethod = typeof ROUTES.listUsers.method; // "GET"Wenn du aus dieser Registry typisierte Fetch-Wrapper, OpenAPI-Schemas oder Berechtigungs-Maps generierst, werden die literalen Typen Teil deiner API-Oberfläche auf Typebene – ganz ohne Laufzeitprüfungen.
satisfies vs. as: nicht dasselbe
as ist eine Assertion – du sagst dem Compiler, dass er dir vertrauen soll, egal was er sieht. Sie unterdrückt Fehler, validiert aber nichts. Genau diese Verwechslung ist der Grund, warum Typfehler bis in die Produktion durchrutschen.
// ❌ as bypasses validation — the compiler accepts nonsense
const broken = {
getUser: { method: "INVALID_METHOD" },
} as Record<string, RouteDefinition>; // No error, but wrong at runtime
// ✅ satisfies catches the mistake at compile time
const broken = {
getUser: { method: "INVALID_METHOD" },
} satisfies Record<string, RouteDefinition>;
// Error: Type '"INVALID_METHOD"' is not assignable to type 'HttpMethod'Setze as nur dort ein, wo du wirklich etwas weißt, das der Compiler nicht wissen kann – etwa bei einem DOM-Query-Ergebnis, dessen Elementtyp sicher feststeht, oder bei einem deserialisierten Payload nach expliziter Laufzeitvalidierung. Nutze satisfies, wenn du Validierung bei erhaltener Inferenz willst.
Kombination mit as const
Wenn du sowohl literale Typen bewahren als auch tiefe Unveränderlichkeit erreichen willst, lassen sich as const und satisfies sauber kombinieren.
const STATUS_CODES = {
ok: 200,
created: 201,
noContent: 204,
badRequest: 400,
unauthorized: 401,
notFound: 404,
serverError: 500,
} as const satisfies Record<string, number>;
// Every value is its literal numeric type: 200, 201, 204...
// AND TypeScript enforces that all values are numbers
type OkCode = typeof STATUS_CODES.ok; // 200 — not number
function isSuccess(code: typeof STATUS_CODES[keyof typeof STATUS_CODES]): boolean {
return code >= 200 && code < 300;
}Die Reihenfolge zählt: Schreibe as const satisfies T, nicht satisfies T as const. TypeScript wertet von links nach rechts aus – as const verengt
zuerst auf Literale, danach validiert satisfies diesen verengten Typ gegen
die Constraint.
Diskriminierte Unions präzise halten
satisfies ist besonders nützlich, wenn jeder Eintrag eines Lookup-Objekts ein Member einer diskriminierten Union ist und das diskriminierende Literal auch im weiteren Verlauf erhalten bleiben muss.
type NotificationHandler =
| { type: "email"; to: string; subject: string }
| { type: "sms"; to: string }
| { type: "push"; deviceToken: string; title: string };
// ❌ Annotation collapses discriminants to the full union
const handlers: Record<string, NotificationHandler> = {
email: { type: "email", to: "", subject: "" },
sms: { type: "sms", to: "" },
push: { type: "push", deviceToken: "", title: "" },
};
// handlers.email.type is "email" | "sms" | "push"
// ✅ satisfies keeps each member precise
const handlers = {
email: { type: "email", to: "", subject: "" },
sms: { type: "sms", to: "" },
push: { type: "push", deviceToken: "", title: "" },
} satisfies Record<string, NotificationHandler>;
// handlers.email.type is "email"
// handlers.sms.type is "sms"
// Downstream switch statements can prove exhaustiveness without assertionsDas spielt eine Rolle, wenn Handler-Objekte in Funktionen fließen, die anhand von type verzweigen. Bei einer aufweitenden Annotation kann der Compiler nicht helfen. Mit satisfies kann er beweisen, dass jeder Zweig erreichbar und der switch vollständig ist.
Ein vollständiges Muster: typsichere Feature-Flag-Registry
Hier ein produktionsreifes Muster, das alles zusammenführt. Die Registry validiert die Struktur, bewahrt die literalen Typen und leitet ihre Key-Union automatisch ab.
interface FeatureFlag {
defaultValue: boolean;
description: string;
rolloutPercentage: number;
}
const FLAGS = {
newDashboard: {
defaultValue: false,
description: "Enables the redesigned analytics dashboard",
rolloutPercentage: 0,
},
streamingExport: {
defaultValue: true,
description: "Uses streaming for large CSV exports",
rolloutPercentage: 100,
},
betaSearch: {
defaultValue: false,
description: "Experimental vector-powered search",
rolloutPercentage: 10,
},
} satisfies Record<string, FeatureFlag>;
// Derived automatically — no manual union to maintain
type FlagName = keyof typeof FLAGS;
// "newDashboard" | "streamingExport" | "betaSearch"
function isEnabled(flag: FlagName, userPercentile: number): boolean {
const { defaultValue, rolloutPercentage } = FLAGS[flag];
return defaultValue || userPercentile <= rolloutPercentage;
}FlagName aktualisiert sich jedes Mal, wenn sich FLAGS ändert. Fügst du einen Key hinzu, wächst die Union. Entfernst du einen, schlägt jede veraltete Referenz schon beim Kompilieren fehl. Ein fehlerhafter Flag-Eintrag ist ein Kompilierfehler – keine Laufzeitüberraschung, die erst im Staging-Deploy auffällt.
Die wichtigsten Erkenntnisse
- Annotationen weiten auf,
satisfiesbewahrt — nutzesatisfies, wenn du Formvalidierung und literale Typen im nachgelagerten Code willst. satisfiesvalidiert;asunterdrückt — verwendeasniemals, um eine Formabweichung zu übertünchen, diesatisfieszu Recht ablehnen würde.as const satisfies Tist die vollständige Kombination — unveränderliche Literale, die trotzdem gegen eine strukturelle Constraint validiert werden.- Key-Unions aus Registries ableiten —
keyof typeof myObjectnachsatisfiesliefert eine präzise, sich selbst pflegende Union ganz ohne manuellen Abgleich. - Members diskriminierter Unions bleiben eng gefasst — die literalen Diskriminanten bleiben erhalten, sodass nachgelagerte
switch-Anweisungen ohne zusätzliche Assertions vollständig erschöpfend sind.


