Skip to main content

The satisfies operator

· 15 min read

TypeScript 4.9 introduces a new operator, satisfies, that allows opting into a different kind of type inference from the type system's default. satisfies brings the best of type annotations and default type inference together in a useful manner. Let's explore the new satisfies operator and why it's useful!

Recap: Default Type Inference

TypeScript's default type inference for objects defaults to inferring primitive values for types, rather than literals. For example, the vibe.mood property in the following value is string, not "happy":

ts
const vibe = {
mood: "happy",
(property) mood: string
};
ts
const vibe = {
mood: "happy",
(property) mood: string
};

TypeScript's default inference is not always optimal since you might want to have a different type. For our vibe.mood, we might have wanted "happy" - or maybe even a union of string literals, such as "happy" | "sad".

Type Declarations

You can use a : type annotation to specify a variable's type. Doing so tells TypeScript to use the annotated type for the variable instead of its default inference.

For example, using a : Vibe type annotation on the vibe variable here switches its mood property from string to "happy" | "sad":

ts
interface Vibe {
mood: "happy" | "sad";
}
 
const vibe: Vibe = {
mood: "happy",
(property) Vibe.mood: "happy" | "sad"
};
 
vibe.mood;
(property) Vibe.mood: "happy" | "sad"
ts
interface Vibe {
mood: "happy" | "sad";
}
 
const vibe: Vibe = {
mood: "happy",
(property) Vibe.mood: "happy" | "sad"
};
 
vibe.mood;
(property) Vibe.mood: "happy" | "sad"

But, this has a flaw too! See the type of vibe.mood after the variable declaration. As developers reading the code, we know vibe.mood should be the more specific ("narrow") "happy", not the more general ("wide") "happy" | "sad".

as const type assertions can also be used to modify types. But those add readonly to all array and object types, which also might not be what you want.

satisfies to the Rescue

TypeScript 4.9's new satisfies operator introduces a happy compromise between : annotations and the default type inference. The syntax for satisfies is to place it after a value and before a name of a type:

ts
someValue satisfies SomeType;
ts
someValue satisfies SomeType;

satisfies applies the best of both worlds:

  • The value must adhere to a specific shape (as with : declarations )
  • Type inference is still allowed to give the value a more narrow shape than the declared type

In other words, satisfies makes sure a value's type matches some type shape, but doesn't widen the type unnecessarily.

We can use satisfies on our vibe object to make sure its mood property allowed to be only is "happy" | "sad", but is still only "happy" afterwards:

ts
interface Vibe {
mood: "happy" | "sad";
}
 
const vibe = {
mood: "happy",
(property) Vibe.mood: "happy" | "sad"
} satisfies Vibe;
 
vibe.mood;
(property) mood: "happy"
ts
interface Vibe {
mood: "happy" | "sad";
}
 
const vibe = {
mood: "happy",
(property) Vibe.mood: "happy" | "sad"
} satisfies Vibe;
 
vibe.mood;
(property) mood: "happy"

By using satisfies, we were able to make sure our vibe matched the Vibe interface, without forcing too wide a type for the mood property.

You can read more about satisfies in the TypeScript 4.9 beta release notes.

Real World Usage: Next.js

The Next.js framework provides an excellent example of how satisfies improves the coding experience for TypeScript developers. Its data fetching patterns such as getServerSideProps and getStaticProps allow users to declare functions with those names in order to retrieve data for a component.

A simplified version of the GetServerSideProps type might look like:

ts
export type GetServerSideProps<Props> = (
context: GetServerSidePropsContext
) => Promise<GetServerSidePropsResult<Props>>;
 
export interface GetServerSidePropsContext {
query: Record<string, string>;
}
 
export type GetServerSidePropsResult<P> =
| { props: P | Promise<P> }
| { notFound: true };
ts
export type GetServerSideProps<Props> = (
context: GetServerSidePropsContext
) => Promise<GetServerSidePropsResult<Props>>;
 
export interface GetServerSidePropsContext {
query: Record<string, string>;
}
 
export type GetServerSidePropsResult<P> =
| { props: P | Promise<P> }
| { notFound: true };

Before satisfies, there was no good way to declare many generic getServerSideProps functions without using explicit type annotations.

Code could use an explicit generic : GetServerSideProps<...> type annotation, but then that would need to be explicitly told a function type parameter for the generic Props type argument:

ts
import { GetServerSideProps } from "next";
 
export interface MyPageData {
locale: string | undefined;
rating: number;
}
 
export const getServerSideProps: GetServerSideProps<MyPageData> = async (
context
) => ({
props: {
locale: context.locale,
rating: Math.random(),
},
});
ts
import { GetServerSideProps } from "next";
 
export interface MyPageData {
locale: string | undefined;
rating: number;
}
 
export const getServerSideProps: GetServerSideProps<MyPageData> = async (
context
) => ({
props: {
locale: context.locale,
rating: Math.random(),
},
});

Leaving out the : GetServerSideProps<...> type annotation meant the getServerSideProps function wouldn't be inferred to satisfy that type. Its context parameter would be implicitly type any without a type annotation. Even if context were to be removed or given a proper : ServerSidePropsContext type annotation, nothing would enforce the function has the right return type:

ts
import { GetServerSidePropsContext } from "next";
 
export const getServerSideProps = (context: GetServerSidePropsContext) => ({
locale: context.locale,
rating: Math.random(),
});
// No error, even though this is returning the wrong object type shape
// (it should have wrapped its props in a `props` property)
ts
import { GetServerSidePropsContext } from "next";
 
export const getServerSideProps = (context: GetServerSidePropsContext) => ({
locale: context.locale,
rating: Math.random(),
});
// No error, even though this is returning the wrong object type shape
// (it should have wrapped its props in a `props` property)

Next.js and satisfies

Now with TypeScript's satisfies operator, we can have the best of both worlds:

  • We can declare our getServerSideProps function with proper parameter and return types
  • We can allow its props generic type to be inferred from data
ts
import { GetServerSideProps } from "next";
 
export const getServerSideProps = (async (context) => {
return {
props: {
locale: context.locale,
rating: Math.random(),
},
};
}) satisfies GetServerSideProps;
 
getServerSideProps;
const getServerSideProps: (context: GetServerSidePropsContext<ParsedUrlQuery, PreviewData>) => Promise<{ props: { locale: string | undefined; rating: number; }; }>
ts
import { GetServerSideProps } from "next";
 
export const getServerSideProps = (async (context) => {
return {
props: {
locale: context.locale,
rating: Math.random(),
},
};
}) satisfies GetServerSideProps;
 
getServerSideProps;
const getServerSideProps: (context: GetServerSidePropsContext<ParsedUrlQuery, PreviewData>) => Promise<{ props: { locale: string | undefined; rating: number; }; }>

By adding satisfies GetServerSideProps after our function, we were able to enforce that it adheres to the proper Next.js interface -- while still allowing it to infer a generic type for its props. Hooray! 🎉

Using satisfies Today

TypeScript 4.9 was released on November 15th, 2022. That means in order to use satisfies, you'll have to install typescript@4.9 or newer:

shell
npm i typescript@latest
shell
npm i typescript@latest

You can also try out the latest ("nightly") version of TypeScript in your browser on typescriptlang.org/play?ts=next.

Here's a TypeScript playground with the Next.js GetServerSideProps example.

Learning TypeScript and satisfies

The satisfies operator was designed and released after the Learning TypeScript book's contents were finalized. satisfies is not mentioned in Learning TypeScript.

Still, Learning TypeScript's The Type System chapter teaches all the concepts you'll need to be able to understand TypeScript's type system. That includes how the type system works, the default type inferences mentioned in this article, and explicitly using : type annotations.

Thanks

Many thanks to John Reilly, Kenny Lin, and Loren Sands-Ramshaw for invaluable help in proofreading this article!

Lastly, appreciation to Oleksandr Tarasiuk who implemented and iterated on the feature with the TypeScript team. Oleksandr is by far the most profilic community contributor to TypeScript and deserves much gratitude and praise. 💙