February 21, 2021
If you're a user of Prisma, or wanted to create a function that changes its return-types based on the caller's parameters, this post is for you.
The TypeScript wizards over at Prisma have achieved very extensive TypeScript support on their PrismaClient, such that the return types for any CRUD can be narrowed depending on what the caller desires.
1...2model User {3 id Int @id @default(autoincrement())4 firstName String5 lastName String6 email String7}8...
1/* ... */23// User contains only id, firstName and email. Not lastName4const user = await prisma.user.findUnique({5 where: {6 id: 07 },8 select: {9 id: true,10 firstName: true,11 email: true12 }13});1415/* ... */
In this example, the caller of prisma.user.findUnique
has specified that they wish their return object to contain only id
, firstName
and email
. And if you actually had this in your IDE and hovered over user
, you would find that your intellisense will give you exactly this narrowed type.
How does Prisma do it?
Disclaimer. I didn't actually look at Prisma's source code, so this may not be exactly the same. But the resulting TypeScript intellisense is identical enough.
In the end, the devs at Prisma are just cleverly using TypeScript's type generics. And it's something you can easily add to your functions in your projects too!
1/* ... */23// Specify a super-set to the key-set you wish to filter from.4// This example is the broadest key-set you can select5type KeyOfType = string | number | symbol;67// Simulate Prisma's parameters, passing the key-set as a generic to be inferred8type FindUniqueParams<TKeys extends KeyOfType> = {9 where: { id: 0 };10 select?: { [key in TKeys]: true };11};1213// The definition of a findOne query on entity type T14// Once the T generic is passed in, TKeys will be inferred to be the key-set of T15// And the return type will be the narrowed prop-set of the T type16type FindUniqueQuery<T extends Object> = <TKeys extends keyof T>(17 params: FindUniqueParams<TKeys>18) => Promise<{ [Prop in TKeys]: T[Prop] } | null>;1920// Example type, taken from above21type User = {22 id: number;23 firstName: string;24 lastName: string;25 email: string;26};2728const findUniqueUser: FindUniqueQuery<User> = async (params) => {29 return await Promise.resolve(30 Object.entries({31 id: 0,32 firstName: "John",33 lastName: "Doe",34 email: "johndoe@domain.com"35 })36 .filter(([key]) => !params.select || params.select[key])37 .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {} as any)38 );39};4041// Simulated Prisma client42const prisma = {43 user: {44 findUnique: findUniqueUser45 }46};4748/* ... */4950// Hovering over user1 will show all props in TypeScript51const user1 = await prisma.user.findUnique({ where: { id: 0 } });5253// Hovering over user2 will show id, firstName and email props in TypeScript54const user2 = await prisma.user.findUnique({55 where: { id: 0 },56 select: {57 id: true,58 firstName: true,59 email: true60 }61});6263/* ... */
Congrats! Now you've improved your function to return a narrowed type, making your DX even better in the future!
Happy coding! -- David Lee