Skip to content
C Codeloom
TypeScript

The TypeScript satisfies Operator Explained

Learn how the TypeScript satisfies operator keeps narrow inferred types while still validating against a wider constraint, with practical examples.

·6 min read · By Yash Kesharwani
Intermediate 8 min read

What you'll learn

  • What problem the satisfies operator solves
  • How it differs from type annotations and as casts
  • Using it to preserve literal types in config objects
  • Common patterns with record types and discriminated unions
  • When NOT to use satisfies

Prerequisites

  • TypeScript basics from /blog/typescript-basic-types
  • Utility types from /blog/typescript-utility-types

The satisfies operator, introduced in TypeScript 4.9, fills a gap between type annotations and type assertions. It checks that an expression conforms to a type, like an annotation does, but it preserves the more specific inferred type of the expression rather than widening it. This makes it the right tool whenever you want validation without losing precision.

The Problem It Solves

Suppose you are defining a configuration object that maps route names to either a string URL or a function returning a string. With a type annotation, TypeScript widens every value to the union, and you lose the specific shape of each entry.

type RouteValue = string | (() => string);
type Routes = Record<string, RouteValue>;

const routes: Routes = {
  home: '/',
  profile: () => '/profile',
};

// routes.profile is typed as RouteValue, not as a function.
// You cannot call it without narrowing first.

A type assertion with as Routes would have the same problem. We get validation but lose the narrower per-key types.

Enter satisfies

The satisfies operator says “check that this value matches the type, but keep the inferred type as-is.” TypeScript validates the shape and reports errors if any value is wrong, but the inferred type retains the specific literal and function shapes.

type RouteValue = string | (() => string);

const routes = {
  home: '/',
  profile: () => '/profile',
} satisfies Record<string, RouteValue>;

// routes.profile is now typed as () => string and can be called directly.
routes.profile();

You get the best of both worlds. The compiler enforces the constraint, but downstream code sees the precise types.

Preserving Literal Types

Without satisfies, object literals widen string fields to string and number fields to number. A type annotation accelerates this widening. With satisfies you keep literal types automatically.

const palette = {
  primary: '#0f62fe',
  danger: '#da1e28',
  success: '#24a148',
} satisfies Record<string, string>;

type ColorKey = keyof typeof palette;
// ColorKey is 'primary' | 'danger' | 'success'

This pattern is common when building type-safe APIs around constants. The values are validated, the keys remain a precise union, and downstream code can use keyof typeof to derive related types.

satisfies Versus as Versus Annotation

Three nearby tools, three different semantics:

A type annotation says “this variable has type T.” TypeScript checks the initializer against T and types the variable as T, possibly widening the inferred type. It is safe but lossy.

An as assertion says “trust me, treat this as T.” TypeScript performs a weak check, and the variable becomes T. It can silently mask bugs because the check is one-sided.

The satisfies operator says “verify that this expression is assignable to T, but type it as whatever I wrote.” The check is bidirectional, and the inferred type stays narrow.

type Config = { mode: 'dev' | 'prod'; port: number };

const a: Config = { mode: 'dev', port: 3000 };
const b = { mode: 'dev', port: 3000 } as Config;
const c = { mode: 'dev', port: 3000 } satisfies Config;

// typeof a.mode is 'dev' | 'prod'
// typeof b.mode is 'dev' | 'prod'
// typeof c.mode is 'dev'

Only c retains the literal ‘dev’ as its mode type.

Discriminated Unions

The satisfies operator shines when defining arrays of discriminated unions. The compiler verifies each entry has a valid discriminant while preserving each element’s specific shape.

type Shape =
  | { kind: 'circle'; radius: number }
  | { kind: 'square'; side: number };

const shapes = [
  { kind: 'circle', radius: 4 },
  { kind: 'square', side: 5 },
] satisfies readonly Shape[];

// shapes[0] is { kind: 'circle'; radius: number }
// shapes[1] is { kind: 'square'; side: number }

Without satisfies, the array would be typed as Shape array, and you would need to narrow with a kind check before accessing radius or side.

Combining With const Assertions

You can combine satisfies with as const for maximum precision. The as const freezes everything to literal and readonly types, then satisfies validates against your constraint.

const STATUSES = {
  pending: { color: 'gray', label: 'Pending' },
  active: { color: 'green', label: 'Active' },
  done: { color: 'blue', label: 'Done' },
} as const satisfies Record<string, { color: string; label: string }>;

type Status = keyof typeof STATUSES;
// Status is 'pending' | 'active' | 'done'

This is the canonical pattern for type-safe lookup tables.

When NOT to Use It

If you genuinely want to widen the type of a variable so that downstream code can reassign it to other valid values of the type, use an annotation. The annotation is also clearer documentation of intent for function parameters and return types.

If you are dealing with values that come from outside the type system, such as JSON parsed from an API or DOM input values, use a runtime validator and a proper type guard. The satisfies operator does not perform any runtime checks.

Do not reach for satisfies inside function signatures. Function parameter and return types should be explicit, both for tooling and for readers.

Common Pitfalls

The validation is one pass at the satisfies boundary. If you mutate the object afterward, the satisfies constraint does not catch the mutation.

const config = { port: 3000 } satisfies { port: number };
// Adding an extra property after the fact is not blocked here,
// because the inferred type already locked in the shape.

If you need to enforce a constraint over time, use an annotation in addition to or instead of satisfies, or freeze the object with Object.freeze.

For more on TypeScript’s type system foundations, see /blog/typescript-basic-types, /blog/typescript-generics-basics, and /blog/typescript-utility-types.

Wrap up

The satisfies operator validates an expression against a type without widening its inferred type. It is the right choice for type-safe config objects, lookup tables, and literal-preserving constants. Use it to get static guarantees while keeping the precision your downstream code needs.