lee.david.cs

TS Magic: Caller Configurable Return Types

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 String
5 lastName String
6 email String
7}
8...
1/* ... */
2
3// User contains only id, firstName and email. Not lastName
4const user = await prisma.user.findUnique({
5 where: {
6 id: 0
7 },
8 select: {
9 id: true,
10 firstName: true,
11 email: true
12 }
13});
14
15/* ... */

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?

Type Generics Magic

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/* ... */
2
3// Specify a super-set to the key-set you wish to filter from.
4// This example is the broadest key-set you can select
5type KeyOfType = string | number | symbol;
6
7// Simulate Prisma's parameters, passing the key-set as a generic to be inferred
8type FindUniqueParams<TKeys extends KeyOfType> = {
9 where: { id: 0 };
10 select?: { [key in TKeys]: true };
11};
12
13// The definition of a findOne query on entity type T
14// Once the T generic is passed in, TKeys will be inferred to be the key-set of T
15// And the return type will be the narrowed prop-set of the T type
16type FindUniqueQuery<T extends Object> = <TKeys extends keyof T>(
17 params: FindUniqueParams<TKeys>
18) => Promise<{ [Prop in TKeys]: T[Prop] } | null>;
19
20// Example type, taken from above
21type User = {
22 id: number;
23 firstName: string;
24 lastName: string;
25 email: string;
26};
27
28const 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};
40
41// Simulated Prisma client
42const prisma = {
43 user: {
44 findUnique: findUniqueUser
45 }
46};
47
48/* ... */
49
50// Hovering over user1 will show all props in TypeScript
51const user1 = await prisma.user.findUnique({ where: { id: 0 } });
52
53// Hovering over user2 will show id, firstName and email props in TypeScript
54const user2 = await prisma.user.findUnique({
55 where: { id: 0 },
56 select: {
57 id: true,
58 firstName: true,
59 email: true
60 }
61});
62
63/* ... */

Congrats! Now you've improved your function to return a narrowed type, making your DX even better in the future!

Happy coding! -- David Lee