satisfies to validate shape while keeping literal types
Published August 18, 2026
type RGB = readonly [red: number, green: number, blue: number];
const palette = {
primary: [15, 98, 254],
danger: [220, 38, 38],
success: [22, 163, 74],
} satisfies Record<string, RGB>;
export const toHex = (key: keyof typeof palette): string =>
palette[key].map((channel) => channel.toString(16).padStart(2, "0")).join("");What
palette is checked against Record<string, RGB> with satisfies instead of a : Record<string, RGB> annotation, so each channel array is still typed as the exact 3-tuple RGB, not widened to the annotation.
Why it matters
Annotating palette: Record<string, RGB> would type-check the values, but it also makes that the type of palette going forward — every key becomes string, so palette.primary doesn't exist as far as the type checker is concerned; only palette[anyString] does, and the specific key list is gone. Leaving the annotation off entirely gets the literal keys back, but loses the check: a typo'd value like [15, 98] (missing a channel) would silently pass. satisfies gets both: TypeScript verifies the expression against Record<string, RGB> and then keeps the inferred type of the expression — the literal keys and the tuple shape — rather than replacing it with the annotation's type.
How it works
RGBis a fixed 3-tuple, notnumber[]— so an array literal with the wrong number of elements fails thesatisfiescheck.palette's inferred type is exactly what you'd get without thesatisfiesclause at all: an object with keys"primary" | "danger" | "success", each mapped toreadonly [number, number, number].toHexrelies on that:keyof typeof paletteis the literal key union, notstring, so passing an unrelated string doesn't type-check.
Gotchas
satisfieschecks assignability, it doesn't change variance rules — a mutable array literal like[15, 98, 254]is still allowed against thereadonlytupleRGB, because mutable arrays are assignable to their readonly counterparts, just not the reverse.- It's easy to reach for
satisfiesand then still add a redundant type annotation next to it. If you need the annotation's type to be the actual type of the variable, use the annotation;satisfiesis specifically for when you want the inferred type to win.
Related
- A plain type annotation (
: Record<string, RGB>) — validates the same shape but widens the variable to that annotation's type, losing the literal keys. as const— also preserves literal types, but doesn't check the value against any external shape the waysatisfiesdoes.