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
true
property 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
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 thefindMany
function. Prisma has the generated typePrisma.PostFindManyArgs
, which you can extend. With the help of the above-definedTruthyKeys
type, we can get our mission done by checking whether theinclude
property contains a truthyauthor
inner 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!