# Typing services

> Type reusable where, select, and include objects, services that take a client or a transaction, plugin clients, and meta.

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

Use these patterns when query objects and data-access functions are shared across files. Result types are covered in [typing results](/docs/guides/typing-results).

## Reuse `where`, `select`, and `include` objects

Argument objects defined outside the call need care, because TypeScript widens `true` to `boolean` in a plain object, and `select` only keeps keys typed as `true`.

> **Do not annotate select objects:** `const select: SelectInput<Schema, 'users'> = { id: true }` compiles, but
> the declared type is `boolean`, so the payload type loses every selected
> key. The same happens with a plain `const select = { id: true }`. The
> rows still contain the fields at runtime; only the type is wrong.

Use `satisfies`. It checks the object against the exported argument type and keeps the literal types:

```ts
import type {
	IncludeInput,
	OrderByInput,
	SelectInput,
	WhereInput,
} from 'better-drizzle';

type Schema = typeof relations;

const publishedOnly = {
	published: true,
	title: { contains: 'drizzle' },
} satisfies WhereInput<Schema, 'posts'>;

const postCard = {
	id: true,
	title: true,
	author: { select: { id: true, name: true } },
} satisfies SelectInput<Schema, 'posts'>;

const withRecentPosts = {
	posts: { orderBy: { id: 'desc' }, take: 3 },
} satisfies IncludeInput<Schema, 'users'>;

const newestFirst = { id: 'desc' } satisfies OrderByInput<Schema, 'posts'>;

const cards = await client.posts.findMany({
	where: publishedOnly,
	select: postCard,
	orderBy: newestFirst,
});
// { id: number; title: string; author: { id: number; name: string } | null }[]
```

`as const` also keeps literals and is enough for flat `select` objects:

```ts
const userSummary = { id: true, name: true } as const;

const users = await client.users.findMany({ select: userSummary });
// { id: number; name: string }[]
```

Prefer `satisfies` for anything that contains an array. `as const` makes arrays `readonly`, and `in`, `notIn`, `AND`, `OR`, and multi-column `orderBy` expect mutable arrays, so the object no longer matches.

A `where` object does not affect the payload type, so a plain annotation is fine there:

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

const where: WhereInput<typeof relations, 'users'> = { active: true };
```

The exported argument types, all generic over `<Schema, Table>`:

* `WhereInput` (structured filters) and `WhereArg` (`WhereInput`, or a Drizzle `SQL` / `SQLWrapper`)
* `SelectInput`, `IncludeInput`, `OrderByInput`, `CursorInput`
* `QueryArgs` for `findMany` / `findFirst` / `findOne` / `findUnique`; `CountArgs`, `ExistsArgs`, `PaginationArgs`, `CursorArgs`
* `CreateArgs`, `CreateManyArgs`, `UpdateArgs`, `UpdateManyArgs`, `UpsertArgs`, `UpsertManyArgs`, `UpdateEachArgs`, `DeleteArgs`, `DeleteManyArgs`
* `CreateDataInput` and `UpdateDataInput` for `data` payloads

The operation argument types (`QueryArgs`, `CreateArgs`, and so on) take an optional third parameter for your [`meta` type](#typed-meta).

## Type services that take a client or a transaction

Export the client type once and use it for every function that queries:

```ts title="db.ts"
export const client = better(db);
export type Db = typeof client;
```

A transaction client is the root client type plus `transaction`, `rollback`, `afterCommit`, and `afterRollback`, so it is assignable to `Db`. The same function then works inside and outside a transaction:

```ts title="posts.service.ts"
import type { Db } from './db';

export async function publishDraft(db: Db, postId: number) {
	const post = await db.posts
		.update({ where: { id: postId }, data: { published: true } })
		.throw();

	return db.users.findUnique({ where: { id: post.authorId } }).throw();
}

await publishDraft(client, 10);

await client.transaction(async (tx) => {
	await publishDraft(tx, 10);
	await publishDraft(tx, 11);
});
```

When a function needs the transaction-only methods, ask for the transaction client type instead. Deriving it from `Db` keeps plugins and `meta` in sync:

```ts
import type { Db } from './db';

type Tx = Parameters<Parameters<Db['transaction']>[0]>[0];

export async function publishAndNotify(tx: Tx, postId: number) {
	await tx.posts.update({ where: { id: postId }, data: { published: true } });
	tx.afterCommit(() => notifySubscribers(postId));
}
```

For a client without plugins or a custom `meta`, the exported types say the same thing directly:

```ts
import type {
	BetterDrizzleClient,
	BetterDrizzleTransactionClient,
} from 'better-drizzle';

type Db = BetterDrizzleClient<typeof relations>;
type Tx = BetterDrizzleTransactionClient<typeof relations>;
```

To accept a single table's delegate, index the client type:

```ts
type UsersRepository = Db['users'];

export function findByEmail(users: UsersRepository, email: string) {
	return users.findUnique({ where: { email } });
}

await findByEmail(client.users, 'ada@example.com');
```

`BetterDrizzleModelDelegate<Schema, Table, Meta, Plugins>` is the same type written out.

> **Clients built with extends():** `typeof client` includes keys added with `client.extends(...)`, and so do
> `$withContext()` clones and the `tx` passed to `transaction()`. See
> [Client extensions](/docs/guides/client-extensions#typing).

## Clients with plugins

Plugins add to the client type: extra operation arguments (`deleted` from [soft delete](/docs/plugins/soft-delete)), model methods (`restore`), and client keys. `typeof client` carries all of them, including on transaction clients:

```ts
import { better } from 'better-drizzle';
import { softDelete } from 'better-drizzle/soft-delete';

export const client = better(db, {
	plugins: [softDelete()],
});
export type Db = typeof client;

export function listUsers(db: Db) {
	return db.users.findMany({ deleted: 'with' }); // typed by the plugin
}
```

`BetterDrizzleClient<typeof relations>` does not know about plugins, so `deleted` is a type error there. Use `typeof client`, or pass the plugin tuple as the third type argument.

## Typed meta

`meta` is `Record<string, unknown>` by default. Pass your own type as the second type argument of `better()`:

```ts title="lib/db.ts"
export type AppMeta = {
	requestId?: string;
	tenantId?: string;
	userId?: number;
};

export const client = better<typeof relations, AppMeta>(db, {
	hooks: {
		afterCreate(ctx) {
			log(ctx.table, ctx.meta?.requestId); // ctx.meta: AppMeta | undefined
		},
	},
});

const scoped = client.$withContext({ requestId: 'req_1' });

await scoped.users.create({
	data: { email: 'ada@example.com', name: 'Ada' },
	meta: { userId: 1 },
});

await client.users.findMany({ meta: { userId: '1' } }); // type error: userId is a number
```

What the type controls:

* `$withContext(meta)` accepts `Partial<AppMeta>`
* per-call `meta` on every operation, raw query, and `transaction(..., { meta })` accepts `AppMeta`
* hook and plugin contexts expose `ctx.meta` as `AppMeta | undefined`

Per-call `meta` is the full `AppMeta`, not a partial, so keep its keys optional unless every call must pass them.

TypeScript does not infer the remaining type arguments once you pass some. With plugins, pass the plugin tuple as well:

```ts
import type { AppMeta } from './lib/db';

const plugins = [softDelete()] as const;

export const client = better<typeof relations, AppMeta, typeof plugins>(db, {
	plugins,
});
```
