Quick Overview
`as` overrides the compiler, and `:` widens the value to the annotation. `satisfies` checks against the type while keeping the narrow inferred literals, which is why it validates config without flattening it.
TypeScript 4.9 introduced the satisfies operator. It solves a classic TypeScript dilemma: validating that an object conforms to a type shape WITHOUT widening its specific inferred property types.
When you annotate a variable explicitly (const obj: Type = {...}), TypeScript widens object properties to general types, losing exact literal inference:
type Colors = "red" | "green" | "blue";
type RGB = [number, number, number];
type Palette = Record<Colors, string | RGB>;
// Explicit annotation widens properties to 'string | RGB'
const paletteAnnotation: Palette = {
red: [255, 0, 0],
green: "#00ff00",
blue: [0, 0, 255],
};
// Error! paletteAnnotation.green might be an RGB tuple according to the wide type!
// paletteAnnotation.green.toUpperCase();
satisfies SolutionThe satisfies operator validates the schema contract while preserving exact literal inference:
const paletteSatisfies = {
red: [255, 0, 0],
green: "#00ff00",
blue: [0, 0, 255],
} satisfies Palette;
// Success! TypeScript REMEMBERS green is a string!
console.log(paletteSatisfies.green.toUpperCase()); // "00FF00"
// Success! TypeScript REMEMBERS red is a tuple!
console.log(paletteSatisfies.red.map((c) => c / 2));
Explicit Annotation (const p: Palette) ──> Validates Schema AND Widens Inferred Types
Satisfies Operator (const p satisfies)──> Validates Schema AND PRESERVES Exact Types!
satisfies with Type Assertions (as): as bypasses validation and forces a type override. satisfies strictly VALIDATES the object and throws errors if invalid!Use satisfies for configuration objects and theme palettes. Next, let's explore Branded Types and Nominal Typing!
TypeScript erases entirely at build time, so a `string` annotation produces no runtime check whatsoever. A value arriving from an API is whatever the server actually sent, regardless of what the annotation claims.
`let x = "GET"` infers `string` because the binding can change; `const x = "GET"` infers the literal `"GET"`. That widening rule is why object properties need `as const` to stay narrow.
`string[]` is any number of strings; `[string, number]` is exactly two, in that order. `readonly` removes the mutating methods at compile time, so `push` becomes a type error rather than a runtime surprise.