Skip to Content
Course content

97: The satisfies Operator

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've been running into a recurring annoyance in my projects lately: the tension between wanting to ensure an object follows a specific schema and wanting TypeScript to remember exactly what's inside that object. Let's look at a real example from a UI project I'm working on.

The Widening Trap

Imagine we're building a theme system. We want our theme objects to have a set of colors, but we want to allow those colors to be either a predefined palette key or any arbitrary CSS color string. Here is how I'd typically set that up:

type Palette = "primary" | "secondary" | "accent";
type Theme = {
  readonly [key: string]: Palette | string;
};

const theme: Theme = {
  brandColor: "primary",
  surfaceColor: "#ffffff",
  borderColor: "secondary",
};

At first glance, this is perfect. The : Theme annotation ensures I don't accidentally put a number or a boolean in there. But here is where it gets frustrating. Let's say I have a function that specifically needs a Palette key to look up a value in a global CSS variable map:

function getPaletteValue(color: Palette) {
  const map = { primary: "#007bff", secondary: "#6c757d", accent: "#ffc107" };
  return map[color];
}

// TypeScript yells at me here:
// Argument of type 'string | Palette' is not assignable to parameter of type 'Palette'.
getPaletteValue(theme.brandColor);

Wait, what? I can clearly see that brandColor is `"primary"`. But because I annotated theme as Theme, TypeScript "widened" the type. It now thinks theme.brandColor could be any string or any Palette key. It lost the specific knowledge that it was exactly `"primary"`.

The Dangerous Workaround

Now, I could use a type assertion. I've seen a lot of developers do this to "silence" the compiler:

getPaletteValue(theme.brandColor as Palette);

Sure, the red squiggle is gone. But I hate this. I've just told TypeScript, "Trust me, I know what I'm doing," which effectively turns off the safety check. If I accidentally changed brandColor to "#ff0000" in the object definition, the assertion would still pass, and I'd have a runtime bug.

Finding the Middle Ground

This is exactly why the satisfies operator was introduced. It allows us to validate that an object matches a type without actually changing the resulting type of that object. Let's try it out by replacing the colon with the satisfies keyword.

const theme = {
  brandColor: "primary",
  surfaceColor: "#ffffff",
  borderColor: "secondary",
} satisfies Theme;

This feels subtle, but it changes everything. I'm telling TypeScript: "Make sure this object matches the Theme interface, but leave the inferred type of the object alone."

Now, let's try that function call again:

// This now works perfectly!
getPaletteValue(theme.brandColor);

TypeScript looks at the object, confirms it satisfies the Theme type, and then preserves the narrowest possible type for each property. It knows theme.brandColor is specifically the literal "primary", which is a valid Palette.

When to use which?

I've started following a simple rule of thumb: use : Type when you want to strictly define what a variable is (especially for function parameters or class properties). Use satisfies Type when you want to verify that a value is correct, but you still want to keep the specific details of that value for later use.




📋 Practical Task

Refactoring the Game Character Stat Validator

You are working on a RPG engine. You have a CharacterStats type that ensures every character has a set of numeric attributes. However, you have a specific function calculateCriticalHit that only works if the strength attribute is specifically a positive number (not just any number).

Currently, the code uses a type annotation, which is causing the compiler to lose the specific literal value of the stats, leading to a type error in the critical hit calculation.

Your Task: Refactor the hero object definition to use the satisfies operator instead of a type annotation. This should allow the calculateCriticalHit function to accept hero.strength without needing a type assertion (as).

type CharacterStats = {
  strength: number;
  agility: number;
  intelligence: number;
};

function calculateCriticalHit(power: 10 | 20 | 30) {
  return power * 2;
}

// FIX THIS LINE:
const hero: CharacterStats = {
  strength: 20,
  agility: 15,
  intelligence: 10,
};

// This line currently throws a type error because 'strength' is widened to 'number'
console.log(calculateCriticalHit(hero.strength));
Rating
0 0

There are no comments for now.

to be the first to leave a comment.