prisma-generator-flavoured-ids
v2.3.1
Published
Generate strongly typed IDs for Prisma Client
Readme
prisma-generator-flavoured-ids
This generator intends to mitigate the issue with weakly-typed IDs on Prisma Schema entities.
Motivation
The following Prisma Schema:
model User {
id String @id @default(uuid())
name String
email String? @unique
Blogposts Blogpost[]
}
model Blogpost {
id String @id @default(cuid())
content String
Author User? @relation(fields: [authorId], references: [id])
authorId String?
}will generate the model and methods related to user with id being of type string. This is not ideal, as it is easy to pass the wrong type of ID to the generated methods, e.g.:
// The called of the method passes `userId`
const deleteBlogpostsForUser = async (id: string) => {
// From within the method, typescript doesn't prevent from using `userId` as a `blogpostId`
await prisma.blogpost.deleteMany({
where: { id },
});
}Solution
To resolve the problem, the generator will overwrite the resulting types with the following:
- Add a branded type for each model ID, e.g.
export interface Flavoring<FlavorT> {
_type?: FlavorT
}
export type Flavor<T, FlavorT> = T & Flavoring<FlavorT>
export type UserId = Flavor<string, 'UserId'>
export type BlogpostId = Flavor<string, 'BlogpostId'>- Change the methods to use the branded type, e.g.
export type UserWhereUniqueInput = Prisma.AtLeast<{
id?: UserId
/// ...
}>
export type UserWhereInput = Prisma.AtLeast<{
id?: StringFilter<"User"> | UserId
/// ...
}>
// and others- Automatically handle foreign key fields - The generator also replaces foreign key field types with the appropriate branded types:
// Before
export type BlogpostPayload = {
id: BlogpostId
authorId: string | null // ❌ weakly typed
}
// After
export type BlogpostPayload = {
id: BlogpostId
authorId: UserId | null // ✅ strongly typed
}This applies to all foreign key fields that reference models with @id fields.
- Strongly type input types - The generator updates all Prisma input types to use branded IDs:
// Before
export type UserCreateInput = {
id?: string
blogposts?: BlogpostCreateNestedManyWithoutAuthorInput
}
export type BlogpostCreateWithoutAuthorInput = {
id?: string
authorId?: string | null
}
// After
export type UserCreateInput = {
id?: UserId // ✅ strongly typed
blogposts?: BlogpostCreateNestedManyWithoutAuthorInput
}
export type BlogpostCreateWithoutAuthorInput = {
id?: BlogpostId // ✅ strongly typed
authorId?: UserId | null // ✅ strongly typed foreign key
}This applies to all input types including CreateInput, UpdateInput, CreateWithout*Input, UpdateWithout*Input, UncheckedCreateInput, UncheckedUpdateInput, and their variants.
In result, the example from above will be prevented by typescript:
import { UserId } from '@prisma/client'
const deleteBlogpostsForUser = async (id: UserId) => {
await prisma.blogpost.deleteMany({
// Typescript will show an error here
where: { id },
});
}Disclaimer
Ideally, Prisma needs to add native support for branded types. If you find this solution useful, please up-vote the Prisma issue
This is a dirty approach, as it relies on the generated code. This library has been used for several months and had to be changed significantly based on the changes Prisma made to its client
Installation and usage
# inside your project's working tree
npm i prisma-generator-flavoured-idsgenerator flavoured_ids {
provider = "prisma-generator-flavoured-ids"
// A path to the generated client - can vary on your setup
output = "node_modules/.prisma/client/index.d.ts"
}Configuration Options
strictFlavours
By default, flavoured types accept any string value (backward compatible behavior). You can enable strict mode to require exact branded types:
generator flavoured_ids {
provider = "prisma-generator-flavoured-ids"
output = "node_modules/.prisma/client/index.d.ts"
strictFlavours = "true"
}Default behavior (strictFlavours = false):
// Any string is accepted
const userId: UserId = "some-string" // ✅ OK
const anotherUserId: UserId = blogpostId // ❌ Error: different flavourStrict mode (strictFlavours = true):
// Only properly branded types are accepted
const userId: UserId = "some-string" // ❌ Error: string is not assignable
const userId: UserId = "some-string" as UserId // ✅ OK: explicit branding required
const anotherUserId: UserId = blogpostId // ❌ Error: different flavour