# Typing results

> How result types follow select and include, and how to name a result type.

URL: https://better-drizzle.com/docs/guides/typing-results

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](/docs/guides/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](/docs/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`:

```ts
// 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
// @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>`                     |

```ts
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:

```ts
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`:

```ts
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:



**better-drizzle**

```ts  
import type { BetterRecord } from 'better-drizzle';

type User = BetterRecord<typeof relations, 'users'>;
```

**Drizzle**

```ts  
import { users } from './schema';

type User = typeof users.$inferSelect;
```

## 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()`](/docs/querying/explain)                                             |
| `ThrowingResult<T>`                                        | what `findFirst` / `findOne` / `findUnique` return: an explainable `T \| null` plus [`.throw()`](/docs/writing/throwing-results) |
| `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                                                                                                          |
