TypeScript satisfies Operator: When to Use It (vs as and Annotations)

Suman SharmaSuman Sharma

There are three ways to connect a value to a type in TypeScript, and they do very different things:

const a: Theme = { ... }; // annotation: "a IS a Theme" const b = { ... } as Theme; // assertion: "trust me, it's a Theme" const c = { ... } satisfies Theme; // check: "make sure it fits Theme, but keep what you know"

satisfies (TypeScript 4.9+) is the one people use least and should use more.

The problem satisfies solves

Say you have a palette where each colour is either a hex string or an RGB tuple:

type Color = string | [number, number, number]; type Palette = Record<"primary" | "danger" | "muted", Color>;

With an annotation, TypeScript checks the object, then forgets the specifics:

const palette: Palette = { primary: "#2563eb", danger: [220, 38, 38], muted: "#64748b", }; palette.primary.toUpperCase(); // ❌ Property 'toUpperCase' does not exist on type 'Color'.

It knows primary is a Color, but not which kind. You wrote a string, and the type threw that away.

With satisfies, you get the check and the precise types:

const palette = { primary: "#2563eb", danger: [220, 38, 38], muted: "#64748b", } satisfies Palette; palette.primary.toUpperCase(); // ✅ string palette.danger[0]; // ✅ number

And mistakes are still caught:

const palette = { primary: "#2563eb", dangr: [220, 38, 38], // ❌ 'dangr' does not exist in type 'Palette' } satisfies Palette; // ❌ also: property 'danger' is missing

Why satisfies is safer than as

as doesn't check that the value is correct. It tells the compiler to stop checking.

type User = { id: string; email: string; name: string }; const user = {} as User; // ✅ no error at all, and every field is missing user.email.toLowerCase(); // 💥 crashes at runtime

TypeScript only rejects an as when the two types don't overlap at all. An empty object "might" be a User, so the assertion compiles. Swap as for satisfies and the compiler lists every missing field.

Rule: use as only when you know something the compiler can't, such as the result of JSON.parse after you've validated it, or a DOM element type. Use satisfies when you want the compiler to check you.

Patterns where it shines

Config objects with literal keys

type Route = { path: string; auth: boolean }; export const routes = { home: { path: "/", auth: false }, dashboard: { path: "/dashboard", auth: true }, settings: { path: "/settings", auth: true }, } satisfies Record<string, Route>; type RouteName = keyof typeof routes; // "home" | "dashboard" | "settings"

With : Record<string, Route> instead, keyof typeof routes would just be string, and routes.typo would compile.

as const satisfies

Combine them for readonly literal values that are still checked:

export const PLANS = { free: { credits: 3, priceUsd: 0 }, pro: { credits: 100, priceUsd: 9 }, } as const satisfies Record<string, { credits: number; priceUsd: number }>; type PlanId = keyof typeof PLANS; // "free" | "pro" type ProCredits = (typeof PLANS)["pro"]["credits"]; // 100

The order matters: as const satisfies T.

Exhaustive maps over a union

type Status = "idle" | "loading" | "success" | "error"; const statusLabel = { idle: "Ready", loading: "Working…", success: "Done", error: "Something went wrong", } satisfies Record<Status, string>;

Add a new Status and this object fails to compile until you add its label. That pairs well with discriminated unions.

Framework configs

Many config files export an object with a known shape:

import type { NextConfig } from "next"; export default { output: "standalone", images: { remotePatterns: [{ hostname: "cdn.example.com" }] }, } satisfies NextConfig;

You get autocomplete and typo-checking without widening anything.

When not to use it

  • Function parameters and return types. Annotate them. They're a contract for callers, and you want the declared type there.
  • Values that will be reassigned to other members of the type. let theme = {...} satisfies Theme keeps the narrow type, so assigning a different valid Theme later can fail. Use an annotation for mutable variables.
  • Public API surfaces where you deliberately want to hide implementation details behind a wider type.

Cheat sheet

You wantUse
"This variable is of type T, full stop"const x: T = …
"Check this fits T, keep the precise type"const x = … satisfies T
"Readonly literals, checked against T"const x = … as const satisfies T
"I know better than the compiler"… as T (rarely)

Frequently asked questions

What does satisfies do in TypeScript?

It checks that an expression is assignable to a type, and reports an error if not, but the variable keeps its own, more specific inferred type instead of being widened to the type you checked against.

What is the difference between satisfies and as?

as tells the compiler to trust you and can hide real mistakes. satisfies asks the compiler to check you, and fails if the value doesn't match. Prefer satisfies whenever you're validating a value rather than overriding a type.

Which TypeScript version added satisfies?

TypeScript 4.9, released in November 2022.

Contact

Get in Touch

Want to call me? book a call and I'll respond in the available time slot. I will ignore all soliciting.