December 01, 2022

How TypeScript 4.9 `satisfies` Your Prisma Workflows

TypeScript's new satisfies operator allows some new, type-safe patterns that previously required lengthy type annotations or tricky workarounds. This article covers several use cases where it helps you with common Prisma-related workflows.

How TypeScript 4.9 `satisfies` Your Prisma Workflows

Table Of Contents

A little background

One of TypeScript's strengths is how it can infer the type of an expression from context. For example, you can declare a variable without a type annotation, and its type will be inferred from the value you assign to it. This is especially useful when the exact type of a value is complex, and explicitly annotating the type would require a lot of duplicate code.

Sometimes, though, explicit type annotations are useful. They can help convey the intent of your code to other developers, and they keep TypeScript errors as close to the actual source of the error as possible.

Consider some code that defines subscription pricing tiers and turns them into strings using the toFixed method on Number:

If we use an explicit type annotation on plans, we can catch the typo earlier, as well as infer the type of the users arguments. However, we might run into a different problem:

When we use an explicit type annotation, the type gets "widened", and TypeScript can no longer tell which of our plans have flat pricing and which have per-user pricing. Effectively, we have "lost" some information about our application's types.

What we really need is a way to assert that a value is compatible with some broad / reusable type, while letting TypeScript infer a narrower (more specific) type.

Constrained identity functions

Before TypeScript 4.9, a solution to this problem was to use a "constrained identity function". This is a generic, no-op function that takes an argument and a type parameter, ensuring the two are compatible.

An example of this kind of function is the Prisma.validator utility, which also does some extra work to only allow known fields defined in the provided generic type.

Unfortunately, this solution incurs some runtime overhead just to make TypeScript happy at compile time. There must be a better way!

Introducing satisfies

The new satisfies operator gives the same benefits, with no runtime impact, and automatically checks for excess or misspelled properties.

Let's look at what our pricing tiers example might look like in TypeScript 4.9:

Now we catch the typo right at the source, but we don't "lose" any information to type widening.

The rest of this article will cover some real situations where you might use satisfies in your Prisma application.

Infer Prisma output types without Prisma.validator

Prisma Client uses generic functions to give you type-safe results. The static types of data returned from client methods match the shape you asked for in a query.

This works great when calling a Prisma method directly with inline arguments:

However, you might run into some pitfalls:

  • If you try to break your query arguments out into smaller objects, type information can get "lost" (widened) and Prisma might not infer the output types correctly.
  • It can be difficult to get a type that represents the output of a specific query.

The satisfies operator can help.

Infer the output type of methods like findMany and create

One of the most common use cases for the satisfies operator with Prisma is to infer the return type of a specific query method like a findUnique — including only the selected fields of a model and its relations.

Infer the output type of the count method

Prisma Client's count method allows you to add a select field, in order to count rows with non-null values for specified fields. The return type of this method depends on which fields you specified:

Infer the output type of the aggregate method

We can also get the output shape of the more flexible aggregate method, which lets us get the average, min value, max value, and counts of various model fields:

Infer the output type of the groupBy method

The groupBy method allows you to perform aggregations on groups of model instances. The results will include fields that are used for grouping, as well as the results of aggregating fields. Here's how you can use satisfies to infer the output type:

Create lossless schema validators

Schema validation libraries (such as a zod or superstruct) are a good option for sanitizing user input at runtime. Some of these libraries can help you reduce duplicate type definitions by inferring a schema's static type. Sometimes, though, you might want to create a schema validator for an existing TypeScript type (like an input type generated by Prisma).

For example, given a Post type like this in your Prisma schema file:

Prisma will generate the following PostCreateInput type:

If you try to create a schema with zod that matches this type, you will "lose" some information about the schema object:

A workaround before TypeScript 4.9 was to create a schemaForType function (a kind of constrained identity function). Now with the satisfies operator, you can create a schema for an existing type, without losing any information about the schema.

Here are some examples for four popular schema validation libraries:

Define a collection of reusable query filters

As your application grows, you might use the same filtering logic across many queries. You may want to define some common filters which can be reused and composed into more complex queries.

Some ORMs have built-in ways to do this — for example, you can define model scopes in Ruby on Rails, or create custom queryset methods in Django.

With Prisma, where conditions are object literals and can be composed with AND, OR, and NOT. The satisfies operator gives us a convenient way to define a collection of reusable filters:

Strongly typed functions with inferred return types

Sometimes you might want to assert that a function matches a special function signature, such as a React component or a Remix loader function. In cases like Remix loaders, you also want TypeScript to infer the specific shape returned by the function.

Before TypeScript 4.9, it was difficult to achieve both of these at once. With the satisfies operator, we can now ensure a function matches a special function signature without widening its return type.

Let's take a look at an example with a Remix loader that returns some data from Prisma:

Here the satisfies operator does three things:

  • Ensures our loader function is compatible with the LoaderFunction signature from Remix
  • Infers the argument types for our function from the LoaderFunction signature so we don't have to annotate them manually
  • Infers that our function returns a Post object from Prisma, including its related comments

Wrapping up

TypeScript and Prisma make it easy to get type-safe database access in your application. Prisma's API is designed to provide zero-cost type safety, so that in most cases you automatically get strong type checking without having to "opt in", clutter your code with type annotations, or provide generic arguments.

We're excited to see how new TypeScript features like the satisfies operator can help you get better type safety, even in more advanced cases, with minimal type noise. Let us know how you are using Prisma and TypeScript 4.9 by reaching out to us on our Twitter.

Don’t miss the next post!

Sign up for the Prisma Newsletter