Cerial

Select + Include

Combine field selection with relation loading — narrow your model fields while also fetching related records.

Cerial uses dynamic return types — the TypeScript return type of a query changes based on the select and include options you pass. This gives you precise control over what data you get back and what types you work with.

All examples use this schema:

object Address {
  street String
  city String
  state String
}

model User {
  id Record @id
  name String
  email Email
  age Int?
  address Address
  shipping Address?
  profileId Record?
  profile Relation? @field(profileId) @model(Profile)
  posts Relation[] @model(Post)
}

model Profile {
  id Record @id
  bio String
  avatarUrl String?
}

model Post {
  id Record @id
  title String
  content String?
  published Bool @default(false)
  createdAt Date @createdAt
  authorId Record
  author Relation @field(authorId) @model(User)
  comments Relation[] @model(Comment)
}

model Comment {
  id Record @id
  body String
  createdAt Date @createdAt
  authorId Record
  author Relation @field(authorId) @model(User)
  postId Record
  post Relation @field(postId) @model(Post)
}

Three Modes

No Select or Include — Full Model

When you pass no select or include, you get the full model type back:

const user = await client.db.User.findOne({
  where: { name: 'Jane' },
});
// user: User | null
// All scalar and object fields included
// Relations NOT included

With Select — Narrowed Fields

Pass select to choose exactly which fields to return. The return type narrows to only the selected fields:

const user = await client.db.User.findOne({
  where: { name: 'Jane' },
  select: { name: true, email: true },
});
// user: { name: string; email: string } | null
// Only name and email — no id, age, address, etc.

With Include — Model + Relations

Pass include to load related records alongside the model. The return type extends with the included relations:

const user = await client.db.User.findOne({
  where: { name: 'Jane' },
  include: { posts: true },
});
// user: (User & { posts: Post[] }) | null
// Full User type plus loaded posts

Relations are never loaded by default. You must explicitly use include to fetch related records. This keeps queries fast and predictable — you always know exactly what data is being fetched.

Combining Select and Include

When both select and include are provided, the result type is the intersection of:

  • The narrowed type from select (only selected scalar/object fields)
  • The included relations from include
const user = await client.db.User.findOne({
  where: { name: 'Jane' },
  select: { id: true, name: true, email: true },
  include: { posts: true },
});
// user: { id: CerialId; name: string; email: string } & { posts: Post[] } | null

The result has only id, name, and email from the model, plus the full posts array.

Object Sub-Select with Include

You can use object sub-field selection in select while also including relations:

const user = await client.db.User.findOne({
  where: { name: 'Jane' },
  select: {
    name: true,
    address: { city: true, state: true },
  },
  include: { profile: true },
});
// user: {
//   name: string;
//   address: { city: string; state: string };
// } & { profile: Profile | null } | null

Include with Select on Relations

Combine top-level select with select inside include to narrow both sides:

const user = await client.db.User.findOne({
  where: { name: 'Jane' },
  select: { name: true },
  include: {
    posts: {
      where: { published: true },
      select: { title: true, createdAt: true },
    },
  },
});
// TypeScript type:
// { name: string } & { posts: { title: string; createdAt: Date }[] } | null

Remember that select inside include is type-level only — full related objects are returned at runtime. Only top-level select affects actual data transfer.

Include with Nested Includes

Top-level select combines with nested includes at any depth:

const post = await client.db.Post.findOne({
  where: { title: 'Hello' },
  select: { id: true, title: true },
  include: {
    author: true,
    comments: {
      include: { author: true },
    },
  },
});
// post: {
//   id: CerialId;
//   title: string;
// } & {
//   author: User;
//   comments: (Comment & { author: User })[];
// } | null

When to Use Both

Combining select and include is useful when you need:

  • API responses — Return only the fields your API consumer needs, plus related data
  • Performance — Reduce the data transferred for the primary model while still loading necessary relations
  • Type safety — Get a precise return type that matches exactly what your code uses
// API endpoint: return user summary with recent posts
const user = await client.db.User.findOne({
  where: { id: userId },
  select: { id: true, name: true, email: true },
  include: {
    posts: {
      where: { published: true },
      orderBy: { createdAt: 'desc' },
      limit: 5,
    },
  },
});
// Clean, typed response with only the data you need

On this page