hor-type-interface · git:20261016.17a86df · 2026-10-16 · sha256 fa68292e2bb4652b
hor-type-interface git:20261016.17a86dfA
Immutable. This exact content never changes and is served at /api/v1/blob/fa68292e2bb4652b.
---
name: hor-type-interface
description: >
Define TypeScript type interfaces in .d.ts declaration files: model interfaces (one file per
model/table under types/models/, in the global `model` namespace) and resolver input/output
types (all in types/graphql.d.ts, in the global `graphql.<category>` namespace, as
<Resolver>Input and <Resolver>Result). Use this skill whenever the user asks to add or change a
model interface, a resolver's Input/Result types, or another shared global type.
---
# Type Interface Definition
A skill for the **`.d.ts` type declarations** that describe an app's data shapes: the **model
interfaces** (one per table) and the **resolver input/output types** (a pair per resolver). Both
are declared as **ambient global namespaces**, so any JSDoc can reference them (`model.User`,
`graphql.user.CreateOrderInput`) without an import. Each model gets **its own file**, merged into
one namespace by TypeScript's declaration merging, and every resolver type lives in the one file
`types/graphql.d.ts`.
> This skill states a **general, project-independent rule** — a refined best practice, not a
> description of any one project's code, so it need not match a given repo's existing notation. The
> directory names, namespaces (`model`, `graphql.<category>`), and example type names (`User`,
> `Order`, `CreateOrderInput`, …) are the **recommended convention with illustrative fakes**; use your
> project's own model and resolver names and never bake its domain concepts or file paths into the
> rule.
## Grand principle: one file per model, one file for resolver types, all in global namespaces
Every model gets its **own `.d.ts` file**, and every resolver type goes into the single file
`types/graphql.d.ts`. Each file declares types into a **shared global namespace**. TypeScript's
**declaration merging** combines the same-named `namespace` blocks into one, so the split costs
nothing at the access site.
- **Why one file per model.** A monolithic `model.d.ts` is a constant merge-conflict point, because
every model change touches the same file. Splitting to `types/models/<ModelName>.d.ts` means two
changes collide **only when they touch the same model**. Adding a model = adding a file.
- **Why one file for resolver types.** A resolver's `Input` / `Result` pair mirrors the schema
(SDL) one-to-one, so it is written in the same change as the SDL and read beside the other
resolver types of its category. One file, `types/graphql.d.ts`, is the place to look, and there is
no per-resolver path to keep in step with the resolver's own file name.
- **Why it still reads as one namespace.** `declare global { namespace model { … } }` in
`User.d.ts` and the same block in `Order.d.ts` **merge** into a single `model` namespace. Consumers
write `model.User` / `model.Order` regardless of which file declared them — the file split is
invisible to readers.
- **Why global (ambient).** Declared under `declare global`, the types need **no import** to be used
in a JSDoc annotation anywhere in the codebase. That is the point of the namespaces — a resolver's
JSDoc just writes `@returns {Promise<graphql.user.CreateOrderResult>}`.
- **Wiring**: the project's `tsconfig` / `jsconfig` `include` must cover the `types/**` directory so
these ambient declarations are picked up project-wide.
**Sample code follows the project's lint style**: `.d.ts` uses no semicolons, 2-space indent, and
interface members are one-per-line (no trailing comma or semicolon). Comments in the files you
generate are English for structural notes; domain notes match the surrounding language.
## 1. The `.d.ts` scaffold
Every declaration file has the same three-line envelope: an `export {}` module marker, then a
`declare global` block, then the `namespace`. The interfaces go inside the namespace.
```ts
export {}
declare global {
namespace model {
// interfaces here
}
}
```
- **`export {}`** makes the file a module (so `declare global` is legal). It exports nothing itself.
- **`declare global { … }`** puts the namespace in the ambient global scope — visible everywhere with
no import.
- The **namespace name is fixed by kind**: `model` for model interfaces ([§2](#2-model-interfaces-one-file-per-table)),
`graphql.<category>` for resolver types ([§3](#3-resolver-inputoutput-types-in-typesgraphqldts)).
## 2. Model interfaces (one file per table)
A model interface maps **1:1 to a table**. Put each in its own file
`types/models/<ModelName>.d.ts`, declaring one interface `<ModelName>` into `namespace model`.
```ts
// types/models/User.d.ts
export {}
declare global {
namespace model {
interface User {
id: number
email: string
displayName: string | null
registeredAt: Date
OrganizationId: number
}
}
}
```
```ts
// types/models/Order.d.ts — a second model, its own file, same namespace
export {}
declare global {
namespace model {
interface Order {
id: number
UserId: number
totalAmount: number
placedAt: Date
canceledAt: Date | null
}
}
}
```
- **One interface per file**, named exactly for the model, so it is reachable as `model.User` /
`model.Order`.
- **Fields mirror the table's columns.** Use the precise scalar type (`number` / `string` / `Date` /
`boolean`); a nullable column is `<type> | null`.
- **A field holding another model's id starts with an uppercase initial** (`UserId`,
`OrganizationId`), mirroring how the ORM's associations are named — this distinguishes a foreign-key
field from a plain scalar at a glance.
- Optionally, a model interface may `extend` a framework base-model interface (if the architecture
provides one for the common columns); keep that base generic, not a hard-coded app path.
## 3. Resolver input/output types (in `types/graphql.d.ts`)
Every resolver's types go into the one file `types/graphql.d.ts`, declared into
`namespace graphql.<category>`, where `<category>` is the API/endpoint group (`user`, `admin`,
`portal`, …). Each resolver declares its **`<Resolver>Input`** and its **`<Resolver>Result`** (the
output / response), written as a pair.
```ts
// types/graphql.d.ts
export {}
declare global {
namespace graphql.user {
interface CreateOrderInput {
productId: number
quantity: number
}
interface CreateOrderResult {
order: model.Order
}
}
namespace graphql.admin {
interface OrdersInput {
limit: number
offset: number
}
interface OrdersResult {
orders: model.Order[]
}
}
}
```
- **Input** is the resolver's argument shape; **Result** is what it returns (the response). A
query uses the same `Input` / `Result` pairing.
- **One `namespace graphql.<category>` block per category**, inside the single `declare global`.
Keep a resolver's `Input` and `Result` next to each other.
- **Reuse model interfaces in the output** — a Result field is typed as `model.<ModelName>`
(`order: model.Order`). The `model` and `graphql.<category>` namespaces are both global, so one
references the other with no import.
- **Resolver-local helper types** (a nested filter input, a row sub-shape) sit in the same block,
beside the pair they belong to, named so they cannot collide with another resolver's.
- **Category = the resolver's endpoint group.** The same resolver name under a different endpoint is
a different namespace (`graphql.user.FooInput` vs `graphql.admin.FooInput`), in the same file.
## 4. Accessing the types
Because the namespaces are global, JSDoc references them directly — no import:
```js
/**
* @param {{
* variables: {
* input: graphql.user.CreateOrderInput
* }
* }} params
* @returns {Promise<graphql.user.CreateOrderResult>}
*/
async resolve ({
variables: {
input,
},
}) {
// input is typed as graphql.user.CreateOrderInput
}
```
- Model shapes are referenced the same way: `@param {model.User} user`.
- This global access is the reason the types are ambient — the split into many files never forces an
import.
## 5. Naming & placement (quick reference)
| Kind | File | Namespace | Interface names | Access |
| --- | --- | --- | --- | --- |
| Model | `types/models/<ModelName>.d.ts` | `model` | `<ModelName>` | `model.<ModelName>` |
| Resolver | `types/graphql.d.ts` | `graphql.<category>` | `<Resolver>Input`, `<Resolver>Result` | `graphql.<category>.<Resolver>Input` |
- A model's file base name is `<ModelName>` (PascalCase); resolver types share the one file.
- Avoid the denied vague identifiers in field names (`data` / `info` / `list` / …); name a field for
what it holds.
## Finishing checklist
- [ ] The type lives in the right file: one model → its own `types/models/<ModelName>.d.ts`; a resolver's types → `types/graphql.d.ts` ([§1](#1-the-dts-scaffold)).
- [ ] File envelope is `export {}` → `declare global` → the correct `namespace` (`model` or `graphql.<category>`).
- [ ] Model interface is 1:1 with a table; fields mirror columns; nullable is `| null`; a foreign-key field starts uppercase ([§2](#2-model-interfaces-one-file-per-table)).
- [ ] `types/graphql.d.ts` declares `<Resolver>Input` + `<Resolver>Result` in the resolver's `graphql.<category>` block, reuses `model.<ModelName>` in outputs, and keeps resolver-local helper types beside the pair ([§3](#3-resolver-inputoutput-types-in-typesgraphqldts)).
- [ ] The `types/**` directory is included in the project's TS config so the ambient declarations resolve.