# TypeScript Type Tree

Every compile emits `quickback.gen.ts`: a compact, type-only tree designed for application code. It is generated directly from the same compiler model that drives routes and OpenAPI, so there is no runtime package and no code to initialize.

```typescript
import type { Api, Qb } from "../quickback.gen";

type Application = Api.applications.Row;
type NewApplication = Api.applications.Insert;
type ApplicationPatch = Api.applications.Update;
type ApplicationId = Api.applications.Id;

type AdvanceInput = Api.applications.actions.advance.Input;
type AdvanceOutput = Api.applications.actions.advance.Output;
type PipelineItem = Api.applications.views.pipeline.Item;
type PipelineResponse = Api.applications.views.pipeline.Response;

type Page = Qb.Page<Application>;
type ApiProblem = Qb.Problem;
```

Feature areas preserve their authored hierarchy:

```typescript
type Booking = Api.events.travel.bookings.Row;
```

## Table types

Each resource table exposes:

| Member | Meaning |
|---|---|
| `Id` | Nominal table-specific primary-key type |
| `Row` | Complete selected wire shape |
| `Insert` | Createable fields, with defaults and nullable fields optional |
| `Update` | Updatable fields, all optional |
| `ListResponse` | `Qb.Page<Row>` |
| `columns.<name>` | Type of one row column |
| `views.<name>.Item` | A view's projected row |
| `views.<name>.Response` | Page plus typed aggregations where declared |
| `actions.<name>.Input` | The action's Zod input contract |
| `actions.<name>.Output` | The declared Zod output contract, or `unknown` |

Primary and foreign keys are branded by default. That makes accidentally passing a candidate ID where a job ID is required a TypeScript error, while both values remain ordinary strings or numbers at runtime.

```typescript
function loadJob(id: Api.jobs.Id) {}

declare const candidateId: Api.candidates.Id;
loadJob(candidateId); // TypeScript error
```

At an untyped boundary, validate the value and cast it once:

```typescript
const jobId = routeParams.id as Api.jobs.Id;
```

## Reading collections

List and view responses share one envelope: rows under `data`, pagination
metadata under `pagination`, and `view` naming the projection behind the
response (`null` for the default collection read).

```typescript
declare const res: Api.applications.ListResponse; // Qb.Page<Api.applications.Row>

res.data;                  // Api.applications.Row[]
res.view;                  // string | null
res.pagination.count;      // rows in this page
res.pagination.hasMore;
res.pagination.nextCursor; // pass back as ?starting_after=
res.pagination.total;      // number | undefined — only when ?total=true
```

`total` and `totalPages` are counted only when the request passes
`?total=true`, so both are optional; every other member is always present. A
view that declares aggregations intersects this shape with a typed
`aggregations` object, and an aggregate-only view returns `{ view,
aggregations }` with no rows or pagination.

## Declaring action outputs

Add an optional `output` Zod schema to make an action end-to-end typed:

{/* doc-compile: skip — project-config prerequisite: the `recruiter+` gate needs `recruiter` in `auth.roleHierarchy`, and the gate compiles every fence against one fixed harness config whose roles are member/admin/owner. The file is otherwise whole. */}

```typescript title="quickback/features/applications/actions/advance.ts"
import { z } from "zod";
import { defineAction } from "../.quickback/define-action";

export default defineAction({
  description: "Advance an application",
  input: z.object({
    nextStatus: z.enum(["screening", "interview", "offer"]),
  }),
  output: z.object({
    id: z.string(),
    status: z.enum(["screening", "interview", "offer"]),
  }),
  access: { roles: ["recruiter+"] },
  async execute({ record, input }) {
    return { id: record.id, status: input.nextStatus };
  },
});
```

The generated feature helper checks `execute` against the declared output. The CLI harvests both schemas locally without invoking `execute`; the resulting contract feeds `quickback.gen.ts`, OpenAPI, and `api-types.gen.ts`.

## Configuration

The artifact is on by default:

```typescript
export default defineConfig({
  // Default: quickback.gen.ts
  types: true,
});
```

Use a custom path, fan out identical copies to colocated apps, disable branding, or turn the artifact off:

```typescript
export default defineConfig({
  types: {
    output: [
      "quickback.gen.ts",
      "apps/web/src/lib/quickback.gen.ts",
    ],
    brandIds: true,
  },
});
```

| `types` value | Behavior |
|---|---|
| `true` or omitted | Write `quickback.gen.ts` |
| `false` | Do not emit the first-party type tree |
| `"src/lib/quickback.gen.ts"` | Write to one custom path |
| `["quickback.gen.ts", "apps/web/src/quickback.gen.ts"]` | Fan out identical copies |
| `{ output, brandIds: false }` | Configure paths and use structural IDs |

`api-types.gen.ts` remains available through `openapi.types` for `openapi-fetch` and third-party OpenAPI tools. The two artifacts are independent: disabling OpenAPI types does not disable the first-party tree.

The compiler also emits `reports/types.parity.gen.ts` when both artifacts exist. Quickback's output-matrix CI compiles that proof separately to require mutual assignability between direct table/action types and the OpenAPI wire types; it is outside the generated application's `src/**/*` build.
