- Published on
Reuse include in Prisma Query with TypeScript satisfies
To avoid repetition!
- Authors
- Name
- Nico Prananta
- Follow me on Bluesky
The satisfies
operator in TypeScript has been available since TypeScript 4.9, but I hadn't had the chance to use it in my production websites until a few days ago, when I finally did.
I have the following Prisma models:
model Participant {
id String @id @default(uuid())
createdAt DateTime @default(now())
participantGroup ParticipantGroup @relation(fields: [participantGroupId], references: [id])
participantGroupId String
}
model ParticipantGroup {
id String @id @default(uuid())
participants Participant[]
course Course[]
@@map("participant_groups")
}
In one of my functions, I needed to:
- Fetch a participant, including the
participantGroup
. - Perform some operations and then update the participant.
- Pass the updated participant to a function, namely, the
doSomethingWithParticipant
function.
The doSomethingWithParticipant
function requires the passed participant object to include the participantGroup
, as follows:
// some-file.ts
const doSomethingFirst = async () => {
let participant = await prismaClient.participant.findFirst({
where: {
id: 'some-uuid',
},
include: {
participantGroup: {
include: {
course: true,
},
},
},
})
// do some other operations
participant = await prismaClient.participant.update({
where: {
id: 'some-uuid',
},
data: {
// update participant's data
},
include: {
participantGroup: {
include: {
course: true,
},
},
},
})
await doSomethingWithParticipant(participant)
}
// some-file2.ts
type ParticipantWithGroup = Prisma.ParticipantGetPayload<{
include: {
participantGroup: true
}
}>
const doSomethingWithParticipant = async (participant: ParticipantWithGroup) => {
// access participant.participantGroup here
}
As you can see, I had to repeat the include
property in both the findFirst
and update
methods. If I didn't, TypeScript would raise an issue near the doSomethingWithParticipant
function. Let's try to refactor the code by assigning the include
property to a variable.
const relationToInclude = {
participantGroup: {
include: {
course: true,
},
},
}
const doSomethingFirst = async () => {
let participant = await prismaClient.participant.findFirst({
where: {
id: 'some-uuid',
},
include: relationToInclude,
})
// do some other operations
participant = await prismaClient.participant.update({
where: {
id: 'some-uuid',
},
data: {
// update participant's data
},
include: relationToInclude,
})
await doSomethingWithParticipant(participant)
}
Great, now I no longer have repeating code. Additionally, TypeScript reminds me if I make a typo:
Image
However, I've lost the autocomplete feature, and I can assign unknown properties to the relationToInclude
object:
const relationToInclude = {
participantGroup: {
include: {
course: true,
},
},
randomStuff: 'true', // this is not correct
}
So, my first thought was to type the relationToInclude
variable directly as Prisma.ParticipantInclude
:
const relationToInclude: Prisma.ParticipantInclude = {
participantGroup: {
include: {
course: true,
},
},
randomStuff: 'what', // great, TypeScript showed error here!
}
It worked, and TypeScript threw an error for randomStuff
because of randomStuff
. However, TypeScript also generated another error at the line where the doSomethingWithParticipant
function is called:
Image
This issue arose because the exact type of Prisma.ParticipantInclude
makes participantGroup
optional, which means it can be undefined, even though the object has participantGroup
at runtime. Since TypeScript perceives participantGroup
in relationToInclude
as potentially undefined, the findFirst
and update
queries infer that the returned participant might not include participantGroup
. Meanwhile, the doSomethingWithParticipant
function expects participantGroup
to be defined.
This is where the satisfies
operator becomes useful. Instead of directly typing relationToInclude
as Prisma.ParticipantInclude
, we can use the satisfies
operator to assert that this object meets the Prisma.ParticipantInclude
structure.
const relationToInclude = {
participantGroup: {
include: {
course: true,
},
},
} satisfies Prisma.ParticipantInclude
This approach allows TypeScript to perform type checking on relationToInclude
, while also enabling it to infer the exact type of relationToInclude
in the findFirst
and update
queries. Consequently, TypeScript will recognize that the updated participant includes participantGroup
.
Image
Are you working in a team environment and your pull request process slows your team down? Then you have to grab a copy of my book, Pull Request Best Practices!