Skip to main content

Overview

Prisma provides industry-leading TypeScript support with fully type-safe database queries, automatic type generation, and compile-time error detection.

Generated Types

Automatic Type Generation

Prisma generates TypeScript types from your schema:
npx prisma generate
This creates types in node_modules/@prisma/client:
import { PrismaClient, User, Post } from '@prisma/client'

const prisma = new PrismaClient()

// User and Post are fully typed
const user: User = await prisma.user.findUnique({
  where: { id: '1' },
})

Model Types

Each model becomes a TypeScript type:
model User {
  id        String   @id @default(cuid())
  email     String   @unique
  name      String?
  createdAt DateTime @default(now())
  posts     Post[]
}
Generates:
type User = {
  id: string
  email: string
  name: string | null
  createdAt: Date
}
Optional fields in Prisma (name String?) become nullable types in TypeScript (name: string | null).

Query Result Types

Basic Queries

// Type: User
const user = await prisma.user.findUnique({
  where: { id: '1' },
})

// Type: User[]
const users = await prisma.user.findMany()

// Type: User | null
const maybeUser = await prisma.user.findFirst({
  where: { email: 'user@example.com' },
})

Select Types

When using select, Prisma narrows the return type:
const user = await prisma.user.findUnique({
  where: { id: '1' },
  select: {
    id: true,
    email: true,
    // name excluded
  },
})

// Type: { id: string; email: string } | null

Include Types

Relations are properly typed:
const user = await prisma.user.findUnique({
  where: { id: '1' },
  include: {
    posts: true,
  },
})

// Type: User & { posts: Post[] } | null

Nested Selections

const user = await prisma.user.findUnique({
  where: { id: '1' },
  select: {
    email: true,
    posts: {
      select: {
        title: true,
        published: true,
      },
    },
  },
})

// Type: {
//   email: string
//   posts: {
//     title: string
//     published: boolean
//   }[]
// } | null

Type Utilities

Prisma Namespace

Access generated types via the Prisma namespace:
import { Prisma } from '@prisma/client'

// Input types for create operations
type UserCreateInput = Prisma.UserCreateInput

// Where clauses
type UserWhereInput = Prisma.UserWhereInput

// Update input
type UserUpdateInput = Prisma.UserUpdateInput

Validator

Create type-safe reusable queries:
import { Prisma } from '@prisma/client'

const userWithPosts = Prisma.validator<Prisma.UserDefaultArgs>()({
  include: { posts: true },
})

type UserWithPosts = Prisma.UserGetPayload<typeof userWithPosts>

// Use in function signatures
function processUser(user: UserWithPosts) {
  console.log(user.posts) // Typed as Post[]
}

Get Payload

Extract types from query objects:
import { Prisma } from '@prisma/client'

const userArgs = {
  select: {
    id: true,
    email: true,
    posts: {
      select: {
        title: true,
      },
    },
  },
} satisfies Prisma.UserDefaultArgs

type UserPayload = Prisma.UserGetPayload<typeof userArgs>

// Type: {
//   id: string
//   email: string
//   posts: { title: string }[]
// }

Advanced Patterns

Generic Repository Pattern

import { Prisma, PrismaClient } from '@prisma/client'

type ModelName = Prisma.ModelName
type Models = {
  [K in ModelName]: PrismaClient[Uncapitalize<K>]
}

class Repository<T extends ModelName> {
  private model: Models[T]

  constructor(
    private prisma: PrismaClient,
    private modelName: T,
  ) {
    this.model = prisma[modelName.toLowerCase()] as Models[T]
  }

  async findById(id: string) {
    return await this.model.findUnique({
      where: { id },
    })
  }
}

const userRepo = new Repository(prisma, 'User')
const user = await userRepo.findById('1') // Typed as User

Conditional Types for Relations

import { Prisma } from '@prisma/client'

type UserArgs<T extends boolean> = T extends true
  ? Prisma.UserGetPayload<{
      include: { posts: true }
    }>
  : Prisma.UserGetPayload<{}>

async function getUser<T extends boolean>(
  id: string,
  includePosts: T,
): Promise<UserArgs<T> | null> {
  return await prisma.user.findUnique({
    where: { id },
    include: includePosts ? { posts: true } : undefined,
  }) as UserArgs<T> | null
}

const userWithPosts = await getUser('1', true)
// Type: User & { posts: Post[] }

const userOnly = await getUser('1', false)
// Type: User

Type-Safe Middleware

import { Prisma } from '@prisma/client'

prisma.$use(async (params, next) => {
  // params.model is typed
  if (params.model === 'User') {
    // params.args is typed based on action
    if (params.action === 'create') {
      const args = params.args as Prisma.UserCreateArgs
      console.log('Creating user:', args.data.email)
    }
  }
  return next(params)
})

Enum Types

Prisma enums become TypeScript enums:
enum Role {
  USER
  ADMIN
  MODERATOR
}

model User {
  id   String @id
  role Role   @default(USER)
}
Usage:
import { Role } from '@prisma/client'

const admin = await prisma.user.create({
  data: {
    email: 'admin@example.com',
    role: Role.ADMIN,  // Type-safe enum value
  },
})

// Type error if using invalid value
const invalid = await prisma.user.create({
  data: {
    email: 'user@example.com',
    role: 'INVALID',  // ❌ Type error
  },
})

JSON Field Types

JSON fields are typed as Prisma.JsonValue:
model User {
  id       String          @id
  metadata Prisma.JsonValue
}
import { Prisma } from '@prisma/client'

type UserMetadata = {
  preferences: {
    theme: 'light' | 'dark'
    notifications: boolean
  }
}

const user = await prisma.user.create({
  data: {
    email: 'user@example.com',
    metadata: {
      preferences: {
        theme: 'dark',
        notifications: true,
      },
    } satisfies UserMetadata,
  },
})

// Type assertion for usage
const metadata = user.metadata as UserMetadata
console.log(metadata.preferences.theme)
Use satisfies to validate JSON structure without losing type information:
const data = {
  theme: 'dark',
  notifications: true,
} satisfies UserMetadata['preferences']

Transaction Types

Batch Transactions

const [user, posts, count] = await prisma.$transaction([
  prisma.user.create({ data: { email: 'user@example.com' } }),
  prisma.post.findMany(),
  prisma.user.count(),
])

// Types: [User, Post[], number]

Interactive Transactions

const result = await prisma.$transaction(async (tx) => {
  // tx is typed as Prisma.TransactionClient
  const user = await tx.user.create({
    data: { email: 'user@example.com' },
  })

  const post = await tx.post.create({
    data: {
      title: 'First Post',
      userId: user.id,
    },
  })

  return { user, post }
})

// Type: { user: User; post: Post }

Raw Query Types

Provide explicit types for raw queries:
type UserWithPostCount = {
  id: string
  email: string
  postCount: bigint
}

const users = await prisma.$queryRaw<UserWithPostCount[]>`
  SELECT 
    u.id,
    u.email,
    COUNT(p.id) as "postCount"
  FROM "User" u
  LEFT JOIN "Post" p ON p."userId" = u.id
  GROUP BY u.id
`

// users is typed as UserWithPostCount[]
users.forEach(u => {
  console.log(u.postCount) // Typed as bigint
})
Raw queries lose automatic type safety. Always provide explicit type annotations.

Compile-Time Validation

Prisma catches errors at compile time:
// ❌ Type error: 'invalidField' doesn't exist
await prisma.user.findMany({
  select: {
    invalidField: true,
  },
})

// ❌ Type error: 'age' expects number, not string
await prisma.user.create({
  data: {
    email: 'user@example.com',
    age: '25',
  },
})

// ❌ Type error: Missing required field 'email'
await prisma.user.create({
  data: {
    name: 'John Doe',
  },
})

Strict Null Checks

Enable strict null checking in tsconfig.json:
tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "strictNullChecks": true
  }
}
Prisma respects null safety:
const user = await prisma.user.findUnique({
  where: { id: '1' },
})

// ❌ Type error: user might be null
console.log(user.email)

// ✅ Null check required
if (user) {
  console.log(user.email)
}

// ✅ Optional chaining
console.log(user?.email)

// ✅ Or use findUniqueOrThrow
const user = await prisma.user.findUniqueOrThrow({
  where: { id: '1' },
})
console.log(user.email) // No null check needed

Client Extensions Type Safety

Extensions maintain type safety:
const extendedPrisma = prisma.$extends({
  model: {
    user: {
      async findByEmail(email: string) {
        return await prisma.user.findUnique({
          where: { email },
        })
      },
    },
  },
})

// Type: User | null
const user = await extendedPrisma.user.findByEmail('user@example.com')

Best Practices

  1. Enable strict mode in TypeScript configuration
  2. Use type inference instead of explicit types when possible
  3. Leverage Prisma.validator for reusable query patterns
  4. Provide types for raw queries explicitly
  5. Use satisfies for JSON field validation
  6. Handle null values properly with strict null checks
  7. Use enums instead of string literals for better type safety
  8. Extract complex types into type aliases for reusability
  9. Use findUniqueOrThrow when you expect a result
  10. Validate at compile time to catch errors early

Type Safety Checklist

  • strict: true in tsconfig.json
  • strictNullChecks: true enabled
  • Type annotations for raw queries
  • Null checks for potentially null values
  • Enums used for categorical data
  • Type utilities used for complex queries
  • satisfies for JSON field validation
  • No any types in production code
  • Proper error handling with typed catches
  • Generated types kept in sync with schema