Typing results
How result types follow select and include, and how to name a result type.
Use this page when you need to name the type of a query result, for example to pass it to a component or return it from a service. For reusable args, service signatures, and typed meta, see typing services.
better-drizzle has no code generation step. Every result type is computed by TypeScript from your Drizzle schema and the arguments of the call. This page shows how that inference works and how to name those types.
All examples use the schema from Getting started (users and posts, related by users.posts and posts.author) and a client created with better(db). The first type argument of every exported type is the relations config returned by defineRelations(...) - typeof relations - written Schema in the signatures below.
Results follow the arguments
The payload type depends on select and include:
// No select or include: the full row
const users = await client.users.findMany();
// { id: number; email: string; name: string; active: boolean }[]
// select: only the keys set to true
const names = await client.users.findMany({
select: { id: true, name: true },
});
// { id: number; name: string }[]
// include: the full row plus the listed relations
const authors = await client.users.findMany({
include: {
posts: { select: { id: true, title: true } },
count: { select: { posts: true } },
},
});
// { id; email; name; active; posts: { id: number; title: string }[]; count: { posts: number } }[]
// A to-one relation is nullable unless Drizzle declares it with optional: false
const drafts = await client.posts.findMany({
include: { editor: true },
});
// { ...post; editor: { id; email; name; active } | null }[]
// r.one.users({ from, to, optional: false }) makes it non-null, unless a
// nested `where` can filter the row out
const posts = await client.posts.findMany({
include: { author: true },
});
// { ...post; author: { id; email; name; active } }[]The rules:
- no
selectorinclude: the table's select model (the same type as Drizzle's$inferSelect) select: only keys whose value is the literaltrue;falseandbooleankeys are left outinclude: every scalar column, plus the listed relations, pluscountwhen requested- to-many relations are arrays; to-one relations are
T | null, orTwhen declared withoptional: falseand not filtered by a nestedwhere - nested
selectandincludeare resolved the same way at each level
Arguments are checked strictly: an unknown key at the top level or inside where, data, create, update, select, include, or orderBy is a type error, and so is combining select and include at the same relation level:
// @ts-expect-error `wher` is not an argument
await client.users.findMany({ wher: { id: 1 } });
// @ts-expect-error `emial` is not a column
await client.users.create({ data: { email: 'a@b.c', emial: 'x' } });
// @ts-expect-error select and include cannot be combined
await client.users.findMany({ select: { id: true }, include: { posts: true } });The method also shapes the result:
| Method | Resolves to |
|---|---|
findMany | Payload[] |
findFirst, findOne, findUnique, update, delete | Payload | null; .throw() resolves to Payload |
create | Payload, or Payload | null with skipDuplicates |
upsert | Payload |
createMany, upsertMany, updateEach, updateMany, deleteMany | BatchResult<Payload> ({ count; data? }) |
count / exists | number / boolean |
paginate | OffsetPaginationResult<Payload> |
cursor | CursorPaginationResult<Payload> |
const user = await client.users.findUnique({ where: { id: 1 } });
// { id; email; name; active } | null
const sameUser = await client.users.findUnique({ where: { id: 1 } }).throw();
// { id; email; name; active }
const {
data,
pagination: { total, hasNext },
} = await client.posts.paginate({
limit: 20,
select: { id: true, title: true },
});
// data: { id: number; title: string }[]Name a result type
From a function
The simplest way to name a payload is to wrap the query in a function and read its return type:
export function listAuthorCards() {
return client.users.findMany({
select: {
id: true,
name: true,
posts: { select: { id: true, title: true } },
},
});
}
export type AuthorCard = Awaited<ReturnType<typeof listAuthorCards>>[number];
// { id: number; name: string; posts: { id: number; title: string }[] }Awaited also unwraps the helper types that findMany and friends return, so the result is the plain payload.
From the arguments: PayloadForArgs
PayloadForArgs<Schema, Table, Args> computes the payload for an arguments object without running or wrapping a query. It is the counterpart of Prisma's UserGetPayload:
import type { PayloadForArgs, QueryArgs } from 'better-drizzle';
import type { relations } from './relations';
type Schema = typeof relations;
export const userWithPosts = {
include: {
posts: {
where: { published: true },
select: { id: true, title: true },
},
},
} satisfies QueryArgs<Schema, 'users'>;
export type UserWithPosts = PayloadForArgs<Schema, 'users', typeof userWithPosts>;
// { id; email; name; active; posts: { id: number; title: string }[] }
const users = await client.users.findMany(userWithPosts);
// UserWithPosts[]Table is the schema key ('users'), not the database table name.
Plain rows: BetterRecord
BetterRecord<Schema, Table> is the full row type, the same as Drizzle's select model:
import type { BetterRecord } from 'better-drizzle';
type User = BetterRecord<typeof relations, 'users'>;Reference
All of these are type exports from better-drizzle:
| Type | Use it for |
|---|---|
BetterDrizzleClient<Schema, Meta, Plugins> | the client returned by better() |
BetterDrizzleTransactionClient<Schema, Meta, Plugins> | the tx passed to transaction() |
BetterDrizzleModelDelegate<Schema, Table, Meta, Plugins> | one table's delegate, such as client.users |
PayloadForArgs<Schema, Table, Args> | the payload for a select / include arguments object |
BetterRecord<Schema, Table> | a full row |
ExplainableResult<T> | what read helpers return: a Promise<T> with .explain() |
ThrowingResult<T> | what findFirst / findOne / findUnique return: an explainable T | null plus .throw() |
ThrowingWriteResult<T> | what update / delete return: a Promise<T | null> plus .throw(), without .explain() |
BatchResult<T> | { count; data? } from batch writes |
OffsetPaginationResult<T> / CursorPaginationResult<T> | paginate() / cursor() results |
BetterTableKey<Schema> / BetterRepositoryKey<Schema> | valid table keys / keys accepted by repository() |
BetterMeta | the default meta type |