Dynamic return type based on input parameter in TypeScript like Prisma

JS - Dec 20 '22 - - Dev Community

Prisma does a good job of type safety

After using TypeORM for many years, I have switched to Prisma recently, as mentioned in one of my posts:

The main reason is that Prisma does a good job of type safety. I will show you the most impressive one for me with classical Post & User schema as below:

model User {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  email     String   @unique
  name      String?
  role      Role     @default(USER)
  posts     Post[]
}

model Post {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  published Boolean  @default(false)
  title     String   @db.VarChar(255)
  author    User?    @relation(fields: [authorId], references: [id])
  authorId  Int?
}
Enter fullscreen mode Exit fullscreen mode

Let’s say you want to find all the published posts, you can easily do that using the below code:

const publishedPosts = await prisma.post.findMany({
    where: { published: true }
  })
Enter fullscreen mode Exit fullscreen mode

The publishedPosts would be typed as below:

const publishedPosts: Post[]
Enter fullscreen mode Exit fullscreen mode

If you also want Prisma eagerly load the relation of the author field, you can use include to do that like below:

const publishedPosts = await prisma.post.findMany({
  where: { published: true },
  include: { author: true },
})
Enter fullscreen mode Exit fullscreen mode

This time publishedPosts would have the type as below:

const publishedPosts: (Post & {
  author: User
})[]
Enter fullscreen mode Exit fullscreen mode

In short words, the same function would have different result types according to the input data.

The benefit is that if the query won’t include author but you try to access it, you will get the type-checking error immediately in IDE as below instead of getting the runtime undefined error when launching it:

type-check-fail

Then you can fix it right away instead of probably investigating and fixing the bug reported.

type-check-success

This was the first time I was aware of this ability of Typescript. If it is the same for you, keep reading. I will show you how to do that in a simple way.

How to achieve that

In a nutshell, it is done by the powerful type system of Typescript. Prisma really takes full advantage of it to achieve a good type safety as you can see. I think you can even test your understanding level of Typescript’s type system by looking at the source code Prisma. So to say, it’s not that easy. Therefore, I will use a much simple self-contained version tweaked from Prisma’s source code to illustrate it.

If you want to test the code yourself, you should prepare one project with the aforementioned Post&User schema. You can get that by running the command below:

npx try-prisma --template typescript/rest-nextjs-api-routes
Enter fullscreen mode Exit fullscreen mode
  1. Since the result depends on the true property of include, so we need to have a way to find the truthy key for an object type which could be done below:

    export type TruthyKeys<T> = keyof {
      [K in keyof T as T[K] extends false | undefined | null ? never : K]: K;
    };
    

    You can simply test it using the below case:

    type TestType = TruthyKeys<{ a: true; b: false; c:null; d:"d" }>;
    

    the TestType would have type as below:

    type TestType: "a" | "d"
    
  2. Let’s create PostGetPayLoad type to infer the result type from the input:

    import { Post, Prisma, User } from "@prisma/client";
    
    type PostGetPayload<S extends Prisma.PostFindManyArgs> = S extends {
      include: any;
    }
      ? Post & {
          [P in TruthyKeys<S["include"]>]: P extends "author" ? User : never;
        }
      : Post;
    

    S is the input parameter passed to the findMany function. Prisma has the generated type Prisma.PostFindManyArgs, which you can extend. With the help of the above-defined TruthyKeys type, we can get our mission done by checking whether the include property contains a truthy author inner property. If so, we will return Post &{ author:User} , Otherwise simply return Post

  3. Finally, you can declare the below dummy function to test the result. It will get you the same type of result as Prisma’s version.

    declare function findMany<T extends Prisma.PostFindManyArgs>(args: T): Array<PostGetPayload<T>>;
    

Easter Eggs

Let’s say we use include but set the false value to author like below:

const publishedPosts = await prisma.post.findMany({
  where: { published: true },
  include: { author: false },
})
Enter fullscreen mode Exit fullscreen mode

What do you think the result type should be? If you still remember the test case of TruthyKeys, You can be sure that it is the same without the no-including case, which will be Post[].

What do you think about the below one?

const parameter = {
    where: { published: true },
    include: { author: false },
  };

const publishedPosts = await prisma.post.findMany(parameter);
Enter fullscreen mode Exit fullscreen mode

Actually, it just extracts the parameter to a variable. Since they are semantically equivalent, they should have the exact same result, right? However, this time the result is different:

const publishedPosts: (Post & {
    author: User;
})[]
Enter fullscreen mode Exit fullscreen mode

Why? With the help of IDE intelligence, it won’t take you too much to find the clue.

How to fix it to return the right type

If you managed to fix it still using the variable way, then congratulations that you get more understanding of the type system of Typescript! 😉


ZenStack is our open-source TypeScript toolkit for building high-quality, scalable apps faster, smarter, and happier. It centralizes the data model, access policies, and validation rules in a single declarative schema on top of Prisma, well-suited for AI-enhanced development. Start integrating ZenStack with your existing stack now!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .