Video

Watch the full video on YouTube:

Satisfies Operator in TypeScript

Introduction

In this fourteenth chapter of our TypeScript series, we meet satisfies, the instruction introduced in TypeScript 4.9 that gives us more flexibility when typing variables and constants.

We already saw type assertion (telling the compiler “I know better what type this is”) and type narrowing (narrowing a type by validating at compile and/or runtime). satisfies sits at a very convenient middle ground: it checks that a value conforms to a type without forcing an annotation that loses concrete information about the value.

The Problem with Traditional Typing

Imagine an API that consumes from two databases: MongoDB (string id, a uid) and Postgres (serial auto-increment id).

type Employee = {
  id: string | number
  name: string
}

// Traditional annotation
const employee: Employee = {
  id: "1",
  name: "Angel"
}

It works, but when typing employee.id. we don’t get autocomplete for string or number methods. The constant’s type was “flattened” to Employee, so TypeScript no longer knows the concrete value is a string.

The conventional solution was type narrowing:

if (typeof employee.id === "string") {
  // now it autocompletes string methods
  employee.id.toUpperCase()
}

How Satisfies Works

With satisfies we no longer annotate before the equals sign; we add the instruction at the end of the value:

const employee = {
  id: "1",
  name: "Angel"
} satisfies Employee

We’re telling it: “this object satisfies, it really implements this particular type”. If the answer is yes, it behaves like traditional typing: name can’t be a number.

Key Difference

Traditional annotation satisfies
Checks type conformance Yes Yes
Preserves the concrete value type No Yes
Autocompletes the real value No Yes
// With satisfies: we keep the literal value
employee.id.toUpperCase() // ok: it's a string

// If we change to id: 1 (no quotes)
employee.id.toFixed(2) // ok: it's a number

// But it still validates Employee:
const bad = {
  id: true, // error: boolean doesn't satisfy string | number
  name: "Angel"
} satisfies Employee

We type while preserving our object’s integrity.

Advanced Case: Routes with Record

The most powerful real-world use appears when we need to preserve the structure (schema) of our data.

Route type definition

type RouteConfig = {
  path: string
  isRequired: boolean | ((employee: Employee) => boolean)
}

The Problem with Traditional Annotation

const routes: Record<string, RouteConfig> = {
  "users-list": {
    path: "/users",
    isRequired: true
  },
  "user-dashboard": {
    path: "users/:id",
    isRequired: (employee) => Number(employee.id) === 2
  }
}

When typing routes. it doesn’t know which keys exist: I can type anything (routes.caramelo). Even though Record knows RouteConfig’s attributes, it doesn’t know the literal keys - that’s why the LSP forces optional chaining or marks non-existent elements as optional.

The Solution with Satisfies

We remove the annotation and use satisfies at the end:

const routes = {
  "users-list": {
    path: "/users",
    isRequired: true
  },
  "user-dashboard": {
    path: "users/:id",
    isRequired: (employee) => Number(employee.id) === 2
  }
} satisfies Record<string, RouteConfig>

Syntactically there’s a difference; structurally it looks the same. But now routes.:

  1. Knows which elements exist (users-list, user-dashboard)
  2. Knows the exact signature of each one (path: string, isRequired: boolean or function)
  3. Any new key is validated against Record<string, RouteConfig>
routes["users-list"].isRequired
// autocomplete: boolean | ((employee: Employee) => boolean)

routes["user-dashboard"].isRequired({ id: 2, name: "Milo" })
// returns boolean according to the function

We interact with the object knowing exactly which structures it has without losing value inference.

When to Use Satisfies (and When Not)

satisfies didn’t arrive to replace traditional typing. It’s a tool for specific situations:

  1. Use satisfies when you need to preserve the data schema: literal keys, exact shapes, autocomplete for configuration structures (routes, feature flags, form schemas)
  2. Use satisfies when the type is a union and you want to verify the value conforms to a particular type without losing the concrete type
  3. Use traditional typing (const x: Type = ...) when you only need the type guarantee and don’t care about losing the value’s concrete shape
  4. Use type narrowing (preferred) or type assertion when you need safe access to attributes and values of a concrete union member

Best Practices

  1. Prefer satisfies over annotating Record<string, T> when the object has known fixed keys: you keep the literal keys
  2. Combine satisfies with literal types for configuration that must be compile-time validated (route paths, feature keys)
  3. Remember the order: value first, satisfies Type at the end (it’s not an annotation on the declaration)
  4. For runtime unions, satisfies validates at compile time; you still need type narrowing for safe runtime access
  5. Don’t use it as a universal replacement: if you don’t need the concrete shape, traditional typing is simpler

Conclusion

In this episode we saw that satisfies checks that a value conforms to a type while preserving the concrete data type. With the Employee example we compared traditional typing (which flattens the shape) against satisfies (which keeps string or number autocomplete), and with RouteConfig we saw how to preserve literal keys of a Record without sacrificing schema validation.

satisfies is ideal when we need to preserve the structure of our data; for other scenarios, traditional typing and type narrowing remain the foundation.

If the explanation wasn’t clear or you have more advanced use cases, leave them in the comments.

Repository

💻 Repository: https://github.com/radagva/ts-example

TypeScript Guide EP13: The Infer KeywordTypeScript Guide EP15: Mapped Types and never