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
constvibe = {mood : "happy",};
ts
constvibe = {mood : "happy",};
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
interfaceVibe {mood : "happy" | "sad";}constvibe :Vibe = {mood : "happy",};vibe .mood ;
ts
interfaceVibe {mood : "happy" | "sad";}constvibe :Vibe = {mood : "happy",};vibe .mood ;
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 addreadonly
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
interfaceVibe {mood : "happy" | "sad";}constvibe = {mood : "happy",} satisfiesVibe ;vibe .mood ;
ts
interfaceVibe {mood : "happy" | "sad";}constvibe = {mood : "happy",} satisfiesVibe ;vibe .mood ;
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 typeGetServerSideProps <Props > = (context :GetServerSidePropsContext ) =>Promise <GetServerSidePropsResult <Props >>;export interfaceGetServerSidePropsContext {query :Record <string, string>;}export typeGetServerSidePropsResult <P > =| {props :P |Promise <P > }| {notFound : true };
ts
export typeGetServerSideProps <Props > = (context :GetServerSidePropsContext ) =>Promise <GetServerSidePropsResult <Props >>;export interfaceGetServerSidePropsContext {query :Record <string, string>;}export typeGetServerSidePropsResult <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 interfaceMyPageData {locale : string | undefined;rating : number;}export constgetServerSideProps :GetServerSideProps <MyPageData > = async (context ) => ({props : {locale :context .locale ,rating :Math .random (),},});
ts
import {GetServerSideProps } from "next";export interfaceMyPageData {locale : string | undefined;rating : number;}export constgetServerSideProps :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 constgetServerSideProps = (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 constgetServerSideProps = (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 constgetServerSideProps = (async (context ) => {return {props : {locale :context .locale ,rating :Math .random (),},};}) satisfiesGetServerSideProps ;getServerSideProps ;
ts
import {GetServerSideProps } from "next";export constgetServerSideProps = (async (context ) => {return {props : {locale :context .locale ,rating :Math .random (),},};}) satisfiesGetServerSideProps ;getServerSideProps ;
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. 💙