All Articles

Clarifying the `satisfies` Operator

Matt Pocock
Matt PocockMatt is a well-regarded TypeScript expert known for his ability to demystify complex TypeScript concepts.

TypeScript's satisfies operator has been out for a while, but it still seems to be a source of confusion that could use some clearing up.

Think of satisfies as another way to assign types to values.

Before we look closer, let's do a review of how to assign types.

First, there's the humble "colon annotation" (we'll go with this slightly medical-sounding name since this concept isn't really given a name in the TS docs).

The : says "this variable is always this type":

const obj: Record<string, string> = {};

obj.id = "123";

When you use a colon annotation, you're declaring that the variable is that type.

That means that the thing you assign to the variable must be that type:

// Type 'number' is not assignable to type 'string'.
const str: string = 123;

It’s possible to give a variable a type that’s wider than the one you initially assign.

let id: string | number = "123";

if (typeof numericId !== "undefined") {
  id = numericId;
}

This is useful when you want to have a default which might later be reassigned.

But colon annotations come with an edge-case downside.

When you use a colon, the type BEATS the value.

In other words, if you declare a wider type than you want, you're stuck with the wider type.

For example, in this snippet you don't get autocomplete on the routes object:

const routes: Record<string, {}> = {
  "/": {},
  "/users": {},
  "/admin/users": {},
};

// No error!
routes.awdkjanwdkjn;

This is the problem satisfies was designed to solve.

When you use satisfies, the value BEATS the type.

This means it infers the narrowest possible type, not the wider type you specify:

const routes = {
  "/": {},
  "/users": {},
  "/admin/users": {},
} satisfies Record<string, {}>;

// Property 'awdkjanwdkjn' does not exist on type
// '{ "/": {}; "/users": {}; "/admin/users": {}; }'
routes.awdkjanwdkjn;

satisfies also protects you from specifying the wrong thing inside your config object.

So, colon annotations and satisfies are equally safe.

const routes = {
  // Type 'null' is not assignable to type '{}'
  "/": null,
} satisfies Record<string, {}>;

Another way you can assign types to variables is the ‘as’ annotation.

Unlike satisfies and colon annotations, using ‘as’ annotations lets you lie to TypeScript.

In this example, you wouldn’t see an error in your IDE but it will break at runtime:

type User = {
  id: string;
  name: {
    first: string;
    last: string;
  };
};

const user = {} as User;

// No error! But this will break at runtime
user.name.first;

There are some limits to the lies– you can add properties to objects, but you can’t convert between basic types.

For instance, you can’t force TypeScript to convert a string into a number…

…unless you use the monstrous ‘as-as’:

// Conversion of type 'string' to type 'number'
// may be a mistake because neither type
// sufficiently overlaps with the other.
const str = "my-string" as number;

const str2 = "my-string" as unknown as number;

Here’s a legit usage of as, where it is used to convert an object to a known type that hasn’t been constructed yet:

type User = {
  id: string;
  name: string;
};

// The user hasn't been constructed yet, so we need
// to use 'as' here
const userToBeBuilt = {} as User;

(["name", "id"] as const).forEach((key) => {
  // Assigning to a dynamic key!
  userToBeBuilt[key] = "default";
});

A word of caution: if you’re using as as your default way to annotate variables, that’s almost certainly wrong!

The code below might look safe, but as soon as you add another property to the User type, the defaultUser will be out of date, and it will not show you an error!

type User = {
  id: string;
  name: string;
};

const defaultUser = {
  id: "123",
  name: "Matt",
} as User;

There’s one last way to give a type to a variable:

Don’t.

That’s not a typo!

TypeScript does an amazing job at inferring types for your variables.

In fact most of the time, you won’t need to type your variables at all:

const routes = {
  "/": {},
  "/users": {},
  "/admin/users": {},
};

// OK!
routes["/"];

// Property 'awdahwdbjhbawd' does not exist on type
// { "/": {}; "/users": {}; "/admin/users": {}; }
routes["awdahwdbjhbawd"];

To recap, we’ve got FOUR ways of assigning a type to a variable:

  • colon annotations
  • satisfies
  • as annotations
  • not annotating and letting TS infer it

With different ways to do similar things, it can get a bit confusing about when to use what.

Simple use cases are where satisfies works best:

type User = {
  id: string;
  name: string;
};

const defaultUser = {
  id: "123",
  name: "Matt",
} satisfies User;

But most of the time, when you want to assign a type to a variable you probably want the type to be wider.

If this example used satisfies, you wouldn’t be able to assign numericId to id:

// colon annotation

let id: string | number = "123";

if (typeof numericId !== "undefined") {
  id = numericId;
}

// satisfies

let id = "123" satisfies string | number;

if (typeof numericId !== "undefined") {
  // Type 'number' is not assignable to type 'string'.
  id = numericId;
}

The rule of thumb is that you should only use satisfies in two specific situations:

  • You want the EXACT type of the variable, not the WIDER type.
  • The type is complex enough that you want to make sure you didn’t mess it up

Total TypeScript Core Volume includes dozens of challenges in this vein that will help you solidify your mental model for type assignments and transformations!

Matt's signature

Clarifying the `satisfies` Operator