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:
Why I chose T3 stack as the full-stack to build the react app
JS for ZenStack ・ Dec 6 '22
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?
}
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 }
})
The publishedPosts would be typed as below:
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:
const publishedPosts = await prisma.post.findMany({
where: { published: true },
include: { author: true },
})
This time publishedPosts would have the type as below:
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:
Then you can fix it right away instead of probably investigating and fixing the bug reported.
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
-
Since the result depends on the
trueproperty ofinclude, 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" -
Let’s create
PostGetPayLoadtype 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;Sis the input parameter passed to thefindManyfunction. Prisma has the generated typePrisma.PostFindManyArgs, which you can extend. With the help of the above-definedTruthyKeystype, we can get our mission done by checking whether theincludeproperty contains a truthyauthorinner property. If so, we will returnPost &{ author:User}, Otherwise simply returnPost -
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 },
})
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);
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;
})[]
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!


Top comments (3)
nice, thanks.
Good to know it could help!
Thank you very much!