Why ‘satisfies’ is a BIG deal

With the most recent Typescript version as of today – 4.9 – among a bunch of features (that I’ll make sure to cover in other posts) we are getting access to the satisfies keyword and that is a big deal! Bigger than you think, at least.

At its core level the purpose of the satisfies keyword is to enforce a constraint on a variable, without changing its type.

The best use case I have seen for the keyword is when working with union types and instead of narrowing the types where it’s being used (eg. in a function), we can instead use the satisfies keyword to avoid narrowing altogether!

Let’s consider the following example:

type RGB = [red: number, green: number, blue: number];
type Color = { value: RGB | string };

const myColor: Color = { value 'red' };

In the above case our Typescript is correct and up until version 4.9 it is what we would have written. In other words, this is a perfectly fine and valid TS example.

However, it has a flaw!

If we wanted to call myColor.value.toUpperCase(), I would actually get an Error thrown. Do you see why?

That’s correct! We need to first narrow the type.

If that’s the first you’re hearing on the topic, you can read a bit more about it here, directly from the Typescript handbook.

Enter, the satisfies keyword

Now that you understand what the baseline use case looks like, let’s take a look at why satisfies is a big deal.

First, let’s rewrite the above code example using our new keyword:

type RGB = [red: number, green: number, blue: number];
type Color = { value: RGB | string };

const myColor = { value: 'red' } satisfies Color;
const myIncorrectColor = { value: 100 } satisfies Color; 

Here we instantiate two variables using the satisfies keyword, instead of directly setting the type for the variable.

The first, myColor is instantiated with the string value “red” and as such satisfies the constraint that the value parameter must be of either RGB or string type.

Typescript can infer that myColor satisfies the constraint of being of a string type! That is a bigger deal than it sounds.

In short, it means wherever we use the variable myColor we can now do what we couldn’t before: myColor.value.toUpperCase(), and it will work without having to do any sort of type narrowing!

That’s pretty cool huh?!

What it actually means it we get two very significant benefits, from writing just one keyword:

  1. We reduce the amount of ‘boilerplate’ code from doing type checks that have little runtime effect.
  2. We reduce the amount of overhead required for maintaining types, as we don’t have to write a a new type for narrow use cases, but can instead rely on Typescript to infer the proper type.

One of the primary reservations I hear towards using satisfies is that it’s the same as writing exact types, or that you have to compromise by not getting all the benefits of writing a newer, more exact type.

Well, I’m here to tell you the opposite! In fact, I think it’s a best-of-both-worlds solution.

So what is myIncorrectColor doing in this example?

It’s the example I brought to show you how we don’t need a more specific type! In fact, if you try to instantiate that very variable, you’ll get an error:

Type 'number' is not assignable to type 'string | RGB'.ts(2322)

That is some pretty powerful feedback to get in your error, by just using one extra keyword, and avoiding the overhead of maintaining more types, whilst reducing boilerplate.

More examples and why it’s convenient

So in short, satisfies just makes things we could already do more convenient. It lets us type literals inline as seen, which reduce the overall amount of code we need to write.

In the above example I didn’t even mention the many use cases when we’re defining typeless objects where we don’t have a “master” type to inherit from – or ‘satisfy’.

So what do we do in those cases?

Well good thing I brought some more examples for you!

Let’s first hypothesize the following thought-up example:

const sizes: Record<'small'|'medium'|'large', any> = { 
    small: '10px', 
    medium: 100,
    large: false
} 

Here we define the object properties as we may have done in the past, if we didn’t want to explicitly create a type for this object.

As evident by the result in the following line, we don’t have much flexibility for defining types inline. Let’s see what this produces:

type Sizes = typeof sizes
type Sizes = {
    small: any;
    medium: any;
    large: any;
}

Here we can see that each of the attributes are defined as any, despite them clearly being instantiated with specific values! We should certainly hope that Typescript could infer types like this, but without the satisfies keyword, it can’t. At least not for now.

But let’s see how that looks and behaves using satisfies:

const sizes = { 
    small: '10px', 
    medium: 100,
    large: false
} satisfies Record<'small'|'medium'|'large', any>

// Each value is inferred to its exact type
type Sizes = typeof sizes
type Sizes = {
    small: string;
    medium: number;
    large: boolean;
}

The only thing that changed in how we define these two, is that we type satisfies and provide the definition after the object definition.

But the value this change provides should be clear from the example above.

sizes is now correctly inferring the type of each attribute associated with the object.

Inferred type narrowing & autocomplete

The quick reader will actually spot something ‘hidden between the lines’ from this, and the first example in this post.

When we use satisfies Color for example, the typescript compiler will check our code and infer the type from our object definition, and give us proper autocomplete for it!

Example:

type RGB = {red: number|string};

const myColor = { red: 'is a string' } satisfies RGB;

In this extremely contrived example, if I would go ahead and type myColor.red I would get autocomplete as if red is a string, because typescript already knows!

So it’s giving me valuable code completion suggestions that will actually work for the code I’m writing, without first having to narrow any types.

This last use case is probably the most powerful one and where I think you should be looking out the most.

Not in terms of ‘look out for danger’, but keep an eye out for when you are defining types you may not need to, and could instead satisfy the Typescript compiler using satisfies. I’m sure as this keyword grows in usage, Typescript and the need for managing many complex types with shared similarities will become increasingly easier.

At the end of a day, Typescript is here to provide robustness and an increased Developer Experience. satisfies seems to me like a tool that could help us a long way down that road.