Dynamic return type based on input parameter in TypeScript like Prisma
Tags: typescript, webdev, javascript, prisma
Published: 2022-12-20 11:40:30

## 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:

{% embed https://dev.to/zenstack/why-i-choose-t3-stack-as-the-fullstack-to-build-the-react-app-1e1k %}

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:

```jsx
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?
}
```

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

```jsx
const publishedPosts = await prisma.post.findMany({
    where: { published: true }
  })
```

The `publishedPosts` would be typed as below:

```tsx
const publishedPosts: Post[]
```

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

```tsx
const publishedPosts = await prisma.post.findMany({
  where: { published: true },
  include: { author: true },
})
```

This time `publishedPosts` would have the type as below:

```tsx
const publishedPosts: (Post & {
  author: User
})[]
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9gnotjrg61x4shah1886.png)

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


![type-check-success](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/amt6ulqad3mb36ymxiwd.png)

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:

```tsx
npx try-prisma --template typescript/rest-nextjs-api-routes
```

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:
    
    ```tsx
    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:
    
    ```tsx
    type TestType = TruthyKeys<{ a: true; b: false; c:null; d:"d" }>;
    ```
    
    the TestType would have type as below:
    
    ```tsx
    type TestType: "a" | "d"
    ```
    

1. Let’s create `PostGetPayLoad` type to infer the result type from the input:
    
    ```tsx
    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`
    
2. 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. 
    
    ```tsx
    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:

```tsx
const publishedPosts = await prisma.post.findMany({
  where: { published: true },
  include: { author: false },
})
```

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?

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

const publishedPosts = await prisma.post.findMany(parameter);
```

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:

```tsx
const publishedPosts: (Post & {
    author: User;
})[]
```

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! 😉

---

We're building [ZenStack](https://zenstack.dev/?utm_campaign=devto&utm_medium=organic&utm_content=dynamic-result-type), a TypeScript toolkit that enhances Prisma ORM with flexible Authorization layer. With auto-generated type-safe APIs/hooks, you can get the same smooth DX in the front end for free.😄