better-drizzle

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 select or include: the table's select model (the same type as Drizzle's $inferSelect)
  • select: only keys whose value is the literal true; false and boolean keys are left out
  • include: every scalar column, plus the listed relations, plus count when requested
  • to-many relations are arrays; to-one relations are T | null, or T when declared with optional: false and not filtered by a nested where
  • nested select and include are 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:

MethodResolves to
findManyPayload[]
findFirst, findOne, findUnique, update, deletePayload | null; .throw() resolves to Payload
createPayload, or Payload | null with skipDuplicates
upsertPayload
createMany, upsertMany, updateEach, updateMany, deleteManyBatchResult<Payload> ({ count; data? })
count / existsnumber / boolean
paginateOffsetPaginationResult<Payload>
cursorCursorPaginationResult<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:

TypeUse 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()
BetterMetathe default meta type

On this page