# How TypeScript 4.9 `satisfies` Your Prisma Workflows URL: https://www.prisma.io/blog/satisfies-operator-ur8ys8ccq7zb Published: 2022-12-01 Authors: Stephen King Tags: education Description: Learn how TypeScript 4.9''s new `satisfies` operator can help you write type-safe code with Prisma TypeScript's new [`satisfies`](https://devblogs.microsoft.com/typescript/announcing-typescript-4-9/#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. ## Table Of Contents [#table-of-contents] * [A little background](#a-little-background) * [Constrained identity functions](#constrained-identity-functions) * [Introducing `satisfies`](#introducing-satisfies) * [Infer Prisma output types without `Prisma.validator`](#infer-prisma-output-types-without-prismavalidator) * [Infer the output type of methods like `findMany` and `create`](#infer-the-output-type-of-methods-like-findmany-and-create) * [Infer the output type of the `count` method](#infer-the-output-type-of-the-count-method) * [Infer the output type of the `aggregate` method](#infer-the-output-type-of-the-aggregate-method) * [Infer the output type of the `groupBy` method](#infer-the-output-type-of-the-groupby-method) * [Create lossless schema validators](#create-lossless-schema-validators) * [Define a collection of reusable query filters](#define-a-collection-of-reusable-query-filters) * [Strongly typed functions with inferred return types](#strongly-typed-functions-with-inferred-return-types) * [Wrapping up](#wrapping-up) ## A little background [#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`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toFixed) method on `Number`: ```typescript const plans = { personal: 10, team: (users: number) => users * 5, enterprie: (users: number) => users * 20, // ^^ Oh no! We have a typo in "enterprise" }; // We can use `Number` methods on `plans.personal` const pricingA = plans.personal.toFixed(2); // We can call `plans.team` as a function const pricingB = plans.team(10).toFixed(2); // ERROR: Property 'enterprise' does not exist on type... const pricingC = plans.enterprise(50).toFixed(2); ``` 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: ```typescript type Plan = "personal" | "team" | "enterprise"; type Pricing = number | ((users: number) => number); const plans: Record = { personal: 10, team: (users) => users * 5, // We now catch this error immediately at the source: // ERROR: 'enterprie' does not exist in type... enterprie: (users) => users * 20, }; // ERROR: Property 'toFixed' does not exist on type 'Pricing'. const pricingA = plans.personal.toFixed(2); // ERROR: This expression is not callable. const pricingB = plans.team(10).toFixed(2); ``` 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 [#constrained-identity-functions] Before TypeScript 4.9, a solution to this problem was to use a ["constrained identity function"](https://kentcdodds.com/blog/how-to-write-a-constrained-identity-function-in-typescript). 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`](https://www.prisma.io/docs/orm/contract-authoring/the-data-contract) 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` [#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: ```typescript type Plan = "personal" | "team" | "enterprise"; type Pricing = number | ((users: number) => number); const plans = { personal: 10, team: (users) => users * 5, // ERROR: 'enterprie' does not exist in type... enterprie: (users) => users * 20, } satisfies Record; // No error! const pricingA = plans.personal.toFixed(2); // No error! const pricingB = plans.team(10).toFixed(2); ``` 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` [#infer-prisma-output-types-without-prismavalidator] 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: ```typescript import { Prisma } from "@prisma/client"; // Fetch specific fields and relations from the database: const post = await prisma.post.findUnique({ where: { id: 3 }, select: { title: true, createdAt: true, author: { name: true, email: true, }, }, }); // TypeScript knows which fields are available: console.log(post.author.name); ``` ```prisma model Post { id String @id @default(cuid()) title String body String createdAt DateTime @default(now()) author Author @relation(fields: [authorId], references: [id]) authorId String } model Author { id String @id @default(cuid()) name String email String posts Post[] } ``` 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` [#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. ```typescript import { Prisma } from "@prisma/client"; // Create a strongly typed `PostSelect` object with `satisfies` const postSelect = { title: true, createdAt: true, author: { name: true, email: true, }, } satisfies Prisma.PostSelect; // Infer the resulting payload type type MyPostPayload = Prisma.PostGetPayload<{ select: typeof postSelect }>; // The result type is equivalent to `MyPostPayload | null` const post = await prisma.post.findUnique({ where: { id: 3 }, select: postSelect, }); ``` ```prisma model Post { id String @id @default(cuid()) title String body String createdAt DateTime @default(now()) author Author @relation(fields: [authorId], references: [id]) authorId String } model Author { id String @id @default(cuid()) name String email String posts Post[] } ``` ### Infer the output type of the `count` method [#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: ```typescript import { Prisma } from "@prisma/client"; // Create a strongly typed `UserCountAggregateInputType` to count all users and users with a non-null name const countSelect = { _all: true, name: true, } satisfies Prisma.UserCountAggregateInputType; // Infer the resulting payload type type MyCountPayload = Prisma.GetScalarType< typeof countSelect, Prisma.UserCountAggregateOutputType >; // The result type is equivalent to `MyCountPayload` const count = await prisma.user.count({ select: countSelect, }); ``` ```prisma model User { id String @id @default(cuid()) name String country String profileViews Int } ``` ### Infer the output type of the `aggregate` method [#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: ```typescript import { Prisma } from "@prisma/client"; // Create a strongly typed `UserAggregateArgs` to get the average number of profile views for all users const aggregateArgs = { _avg: { profileViews: true, }, } satisfies Prisma.UserAggregateArgs; // Infer the resulting payload type type MyAggregatePayload = Prisma.GetUserAggregateType; // The result type is equivalent to `MyAggregatePayload` const aggregate = await prisma.user.aggregate(aggregateArgs); ``` ```prisma model User { id String @id @default(cuid()) name String country String profileViews Int } ``` ### Infer the output type of the `groupBy` method [#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: ```typescript import { Prisma } from "@prisma/client"; // Create a strongly typed `UserGroupByArgs` to get the sum of profile views for users grouped by country const groupByArgs = { by: ["country"], _sum: { profileViews: true, }, } satisfies Prisma.UserGroupByArgs; // Infer the resulting payload type type MyGroupByPayload = Awaited< Prisma.GetUserGroupByPayload >; // The result type is equivalent to `MyGroupByPayload` const groups = await prisma.user.groupBy(groupByArgs); ``` ```prisma model User { id String @id @default(cuid()) name String country String profileViews Int } ``` ## Create lossless schema validators [#create-lossless-schema-validators] Schema validation libraries (such as a [zod](https://github.com/CarterGrimmeisen/zod-prisma) or [superstruct](https://github.com/ianstormtaylor/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 model Post { id Int @id @default(autoincrement()) title String content String? published Boolean @default(false) } ``` Prisma will generate the following `PostCreateInput` type: ```typescript export type PostCreateInput = { title: string; content?: string | null; published?: boolean; }; ``` If you try to create a schema with [zod](https://github.com/colinhacks/zod) that matches this type, you will "lose" some information about the schema object: ```typescript const schema: z.ZodType = z.object({ title: z.string(), content: z.string().nullish(), published: z.boolean().optional(), }); // We should be able to call methods like `pick` and `omit` on `z.object()` schemas, but we get an error: // TS Error: Property 'pick' does not exist on type 'ZodType'. const titleOnly = schema.pick({ title: true }); ``` A workaround before TypeScript 4.9 was to create a [`schemaForType` function](https://github.com/colinhacks/zod/discussions/667) (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: ```typescript import { Prisma } from "@prisma/client"; import { z } from "zod"; const schema = z.object({ title: z.string(), content: z.string().nullish(), published: z.boolean().optional(), }) satisfies z.ZodType; type Inferred = z.infer; ``` ```typescript import { Prisma } from "@prisma/client"; import { boolean, Describe, Infer, nullable, object, optional, string } from "superstruct"; const schema = object({ title: string(), content: optional(nullable(string())), published: optional(boolean()), }) satisfies Describe; type Inferred = Infer; ``` ```typescript import { Prisma } from "@prisma/client"; import { boolean, InferType, object, ObjectSchema, string } from "yup"; const schema = object({ title: string().required(), content: string().nullable(), published: boolean(), }) satisfies ObjectSchema; type Inferred = InferType; ``` ```typescript import { Prisma } from "@prisma/client"; import { pipe } from "fp-ts/lib/function"; import * as D from "io-ts/Decoder"; const schema = pipe( D.struct({ title: D.string, }), D.intersect( D.partial({ content: D.nullable(D.string), published: D.boolean, }) ) ) satisfies D.Decoder; type Inferred = D.TypeOf; ``` ### Define a collection of reusable query filters [#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](https://guides.rubyonrails.org/active_record_querying.html#scopes) in Ruby on Rails, or create [custom queryset methods](https://docs.djangoproject.com/en/4.1/topics/db/managers/#calling-custom-queryset-methods-from-the-manager) 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: ```typescript const { isPublic, byAuthor, hasRecentComments } = { isPublic: () => ({ published: true, deletedAt: null, }), byAuthor: (authorId: string) => ({ authorId, }), hasRecentComments: (date: Date) => ({ comments: { some: { createdAt: { gte: date }, }, }, }), } satisfies Record Prisma.PostWhereInput>; const posts = await prisma.post.findMany({ where: { AND: [isPublic(), byAuthor(userID), hasRecentComments(yesterday)], }, }); ``` ### Strongly typed functions with inferred return types [#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: ```typescript import { json, LoaderFunction } from "@remix-run/node"; import invariant from "tiny-invariant"; import { prisma } from "~/db.server"; export const loader = (async ({ params }) => { invariant(params.slug, "Expected params.slug"); const post = await prisma.post.findUnique({ where: { slug: params.slug }, include: { comments: true }, }); if (post === null) { throw json("Not Found", { status: 404 }); } return json({ post }); }) satisfies LoaderFunction; export default function PostPage() { const { post } = useLoaderData(); return (

{post.title}

{post.body}
    {post.comments.map((comment) => (
  • {comment.body}
  • ))}
); } ``` 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 [#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](https://dev.to/prisma/productive-development-with-prisma-s-zero-cost-type-safety-4od2), 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](https://pris.ly/x).