- name
- typescript-generator
- description
- Design of the ejected Redocly `typescript` client generator. Read it, and update it, before changing generators/typescript/.
# The `typescript` generator — its skill
This file is the DESIGN of your ejected `typescript` generator (`generators/typescript/`):
**to change the generator, edit this skill first, then make the code match it** — a diff
to `generators/typescript/` that has no covering sentence here is incomplete.
## What it emits
The typed TypeScript client itself: model types with JSDoc, type guards, the `Ops`
type map, the `OPERATIONS` descriptor table, a `client` instance, one binding per
operation, and the runtime — embedded in the file (`runtime: inline`, the default) or
written as real modules in a `runtime/` folder beside it that the client imports
relatively (`runtime: module`).
With `client.telemetry: on`, it also writes `<stem>.telemetry.ts`: the usage telemetry
middleware. The module exports `telemetry()` and `flush()`, and the application registers
it with `use(telemetry({ sdkName, sdkVersion, client }))`.
## Design decisions that must hold
- **Descriptor-driven:** generated code is DATA (`OPERATIONS` + `Ops`) plus wiring;
request behavior lives in the runtime, never in per-operation code.
`satisfies Record<string, OperationDescriptor>` is the version-skew guard.
- **`single` vs `split`:** split derives `<stem>.schemas.ts` (types, enums, guards) and
an entry that `export *`s it; the entry type-imports only the schema names it
references (`collectEntrySchemaRefs`).
- **Zero runtime dependencies.** `Date`, `Blob`, `fetch` — nothing else.
- **Timeouts need no platform statics.** `send()` uses `AbortSignal.timeout()` and
`AbortSignal.any()` where they exist and builds the same signal from an `AbortController`
and `setTimeout` where they do not (Chrome before 116, Safari before 17.4, Firefox before
124). A timed-out attempt is recognized by its own timeout signal having fired, never by
the abort reason's name, because Chrome 103 to 123 abort a timeout with `AbortError`.
- **Names are collision-safe:** `packageIdents` seeds every reserved wiring name before
any operation is sanitized, so renames are deterministic (`configure` → `configure_2`).
A rename becomes part of the SDK's public API, so the warning must say WHICH cause it
is and what the publisher can do: a duplicate `operationId` in the description (fix the
description — the only real fix), a name that isn't a valid identifier, or a clash with
a name the generated module already declares. A vague "collides or is invalid" message
leaves the publisher unable to act.
- **One operation, one function, one input shape.** The module-level names are bindings
of the client's own methods (`export const { getOrder } = client;`), never wrappers, so
`getOrder` and `client.getOrder` cannot disagree about their arguments. `argsStyle`
shapes the method itself: `grouped` (the default) namespaces the inputs by transport
layer — `path`, `query`, `headers`, `cookies`, `body` — and `flat` merges them into one
object, which the runtime converts back using the descriptor's own parameter list. An
operation whose merged names would collide keeps the grouped shape.
- **Stream bodies pass through.** A request body that is a `ReadableStream`, or any
async iterable of bytes (a Node `Readable`), is a pass-through upload: `send()` hands it
to `fetch` untouched with `duplex: 'half'` and makes exactly one attempt, because a
consumed stream cannot be replayed, whatever the retry policy says. The whole-attempt
`timeout` still applies; callers pass `timeout: 0` or a budget that fits the upload per
call. The body type of an untyped `multipart/form-data` operation is
`FormData | ReadableStream`, and of a binary operation (`isBinaryContentType`:
`application/octet-stream`, `application/pdf`, images, audio, video, fonts, zip)
`Blob | ArrayBuffer | ReadableStream`. The client stays free of Node types: a Node
request becomes a web stream with `Readable.toWeb(request)`. Typed multipart objects
are unaffected.
- **The caller's `Content-Type` wins; the description's fills the gap.** For a
pass-through body other than `FormData` or `URLSearchParams` (whose type `fetch`
derives, boundary included), `send()` sets the operation's declared content type only
when the caller set none, the same rule the JSON branch already follows. A forwarded
multipart boundary therefore survives, and a binary upload is never sent untyped.
- **`onSettled` sees every sent call once.** `send()` calls it after the last attempt
with the final response, the error, or both, the attempt count, and the duration from
the first attempt to the end of the `onResponse` hooks on the final response. A call
that fails in `onRequest` or in body serialization does not reach it. A call whose
signal is already aborted reaches it with 0 attempts.
Every hook runs. The first hook error rejects a successful call; on a failed call, the
original error wins.
- **Telemetry is opt-in at generation time.** Without `telemetry: on`, the output holds
no telemetry code, and the client file does not change. An event holds only the
operation id, the counts, the window, and the SDK identity. The kill switches and a
browser turn the middleware off. The module embeds on its own, not in
`RUNTIME_SOURCES`, so `telemetry` and `flush` never become reserved names of a client.
- **Throw mode returns the body**; `{ envelope: true }` opts into
`{ data, headers, response }` with typed declared headers. Result mode returns
`{ data, error, response }` and ignores `envelope`.
## The stage files
One file per stage of the emit, same skeleton as the other generators:
`types.ts` renders type text (`tsType`, JSDoc, the model type aliases);
`operations.ts` the per-operation surface (the `Ops` map, the `<Op>*` aliases, the
input shapes, `flatInputShape`);
`descriptor.ts` the `OPERATIONS` wire table and the collision-safe `packageIdents`;
`client.ts` the assembly (single/split entries, the runtime needs, the module-mode
runtime files);
`type-guards.ts`, `response-headers.ts`, `operation-types.ts`, `operation-signature.ts`
the narrower questions their names state;
`banner.ts` the generated-by header and title comment;
`inline-runtime.ts` the runtime assembly for both modes. The runtime's real sources ship
inside the package and reach the generator through
`@redocly/client-generator/runtime-sources` (in this repo they live in `runtime/` beside
these files). Naming and string escaping live in the TypeScript printer
(`@redocly/client-generator/printers/typescript`).
The telemetry middleware ships the same way, as `TELEMETRY_RUNTIME_SOURCE` (in this repo
it lives in `telemetry/`); `index.ts` writes it with the contract import pointed at the
client entry and the generator version baked in.
## Ejecting it
`redocly eject-generator typescript` copies this generator's TypeScript source folder to
`generators/typescript/` — the stage files above, exactly as we wrote them. Imports stay
package specifiers: `@redocly/client-generator` (the toolkit and IR types),
`@redocly/client-generator/printers/typescript` (naming and text mechanics), and
`@redocly/client-generator/runtime-sources` (the runtime sources it embeds). Running a
`.ts` generator uses Node's own type stripping (Node 22.18, 23.6, or newer), and newer
built-in versions merge into your copy per file with
`redocly eject-generator typescript --update`.
It is the largest of them, so reach for the smaller paths first when they fit:
`client.setup` bakes publisher defaults into the generated client, and middleware or
`configure()` change behavior at run time rather than generation time.
- **It documents itself.** With `client.docs` (or `--docs`), the `docs` hook writes
`<stem>.typescript.md`: the security schemes, then one section per operation with its parameters,
body, response type, and behavior notes. The call snippets come from this generator's own
`sample` hook, so the page can only show the syntax of the SDK beside it, and the layout
comes from `renderReferencePage` in the authoring toolkit — reachable from an ejected copy
through `@redocly/client-generator`. Pagination on the page is decided by
`paginationRuleFor`, the same helper this generator resolves pagination with.
## The modify loop
1. Edit this skill: state the new behavior or decision.
2. Make `generators/typescript/` match it.
3. Run `redocly generate-client` and inspect the `git diff` of the generated output —
generated files are never hand-edited.
Newer built-in versions merge in with `redocly eject-generator typescript --update`.
View on GitHub