Skip to content

feat: export a companion interface alongside each payload class#421

Open
jonaslagoni wants to merge 10 commits into
mainfrom
enable_interface_for_payloads
Open

feat: export a companion interface alongside each payload class#421
jonaslagoni wants to merge 10 commits into
mainfrom
enable_interface_for_payloads

Conversation

@jonaslagoni

@jonaslagoni jonaslagoni commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

What

Give every generated object payload a plain-data companion interface <Name>Interface, exported alongside the class, and widen the payload input sites across all channel/client protocols to accept <Name>Interface | <Name> — normalizing to a class instance before .marshal(). This is the exact ergonomic #413 shipped for parameters, now applied to payloads.

Callers can pass a plain object wherever a payload is accepted — no new <Name>(...) required:

// Both accepted by every generated publish/request helper:
await publishToUserSignedup({ message: { displayName: 'Jane', email: 'jane@example.com' }, nc });
await publishToUserSignedup({ message: new UserSignedUp({ displayName: 'Jane', email: 'jane@example.com' }), nc });

How

  • Payload class preset (payloadClassPreset) prepends interface <Name>Interface {…} and retypes the constructor to constructor(input: <Name>Interface), reusing the existing buildParametersInterfaceBody. It's inserted after TS_COMMON_PRESET so it augments (not replaces) the validation/union/primitive chain and never touches class.additionalContent.
  • Shared export rewrite (withCompanionInterfaceExport) rewrites export { <Name> }export { <Name>, <Name>Interface }, gated on the interface actually being present. parameters.ts was refactored to share this helper so the two generators can't diverge.
  • Channel/client widening via new payloadUnionType / payloadInstanceExpression / renderPayloadNormalization emitters (object payloads delegate to the parameter primitives for a single-sourced format; non-object payloads pass through unchanged). Widened input sites: NATS core/jetstream/request, Kafka, MQTT, AMQP queue/exchange, WebSocket, and HTTP client. Object payloads also get a dual {Class, ClassInterface} import; a messageUnionType field is threaded through the render pipeline so the NATS client wrapper accepts the union too (HTTP client inherits it via its context type).

What's NOT changed

  • Non-object payloads (unions/primitives/arrays/enums) — no interface, single-symbol export, untouched.
  • Receive sites — subscribe/onMessage callbacks, request/reply return types, HTTP responses stay class-typed.
  • Callback-return input sites — NATS coreReply return type and EventSource sendEvent stay class-typed (matches feat: export a companion interface alongside each parameter class #413).
  • No new Zod config field — the interface is always emitted, so schemas/ are unchanged.

Testing

  • Unit (687 tests / 78 snapshots): object payloads gain interface + retyped ctor + dual export; const discriminators correctly omitted from the interface; non-object payloads unchanged. Per-protocol tests assert the widened input type + normalization; excluded sites verified untouched.
  • Blackbox (12 suites): generated code type-checks across every config × input combination.
  • Runtime: a parity spec asserts plain-object ≡ class-instance .marshal() output; HTTP client, WebSocket, and NATS/Kafka/MQTT/AMQP/EventSource broker suites pass against live Docker brokers.
  • Docs section added to docs/generators/payloads.md; the ecommerce-asyncapi-payload example regenerated and updated to demonstrate the plain-object ergonomic.

Compatibility (not a breaking change)

Regenerating produces a diff, but the public-facing interface only gains or widens — nothing is removed or narrowed — so existing consumer source code keeps compiling without changes:

  • The payload class ctor moves from an inline anonymous type to a structurally identical named interface (new <Name>({...}) still compiles).
  • The export adds <Name>Interface (additive).
  • Publish/request inputs and the HTTP payload field widen from <Name> to <Name>Interface | <Name> — callers passing a class instance still type-check.

So this is a backward-compatible feature (feat: → minor), matching #413's compatibility posture. (The only theoretical exception is exotic type-level reflection that pins a widened parameter to exactly <Name> — not a normal usage pattern.)

🤖 Generated with Claude Code

jonaslagoni and others added 9 commits July 16, 2026 13:06
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Add payloadClassPreset() and shared withCompanionInterfaceExport() to utils.ts
- Refactor parameters.ts to use the shared export-rewrite helper
- Insert payloadClassPreset() after TS_COMMON_PRESET in the payload chain
- Apply the companion-interface export rewrite across all payload loops

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Add payloadUnionType/payloadInstanceExpression/renderPayloadNormalization emitters
- Dual {Class, ClassInterface} import for object payloads in addPayloadsToDependencies
- Widen publish/request input sites: NATS core/jetstream/request, Kafka, MQTT,
  AMQP queue/exchange, WebSocket, HTTP client; normalize before marshal
- Thread messageUnionType through render types so NATS client publish wrappers
  accept the Interface | Class union (HTTP client inherits via its context type)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Add 'Companion Interface' section to docs/generators/payloads.md
- Regenerate ecommerce-asyncapi-payload example with companion interfaces
- Demonstrate plain-object publish ergonomics in the example (index.ts + README)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Regenerated test/runtime/typescript/src with companion interfaces on object
payloads and widened publish/request input sites. Verified: payloads parity
spec, HTTP client (114), WebSocket, NATS/Kafka/MQTT/AMQP/EventSource broker
suites all pass. (Pre-existing FormatTime env failure and flaky NATS
request/reply-under-parallel-load are unrelated to this change.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- payload.spec: object payloads (SimpleObject2, SimpleObject) gain interface +
  retyped ctor + dual export; const 'type' correctly omitted from the interface
- channels.spec: OpenAPI HTTP client Pet payload widens (import, context payload
  field, body normalization); non-object response + receive sites unchanged

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The object-payload branch of payloadUnionType/payloadInstanceExpression/
renderPayloadNormalization now delegates to the parameter emitters, single-
sourcing the Interface|Class union and instanceof-guard format while keeping
the object-vs-module gate explicit. (Shared withCompanionInterfaceExport export
helper was already consolidated in Phase 3.) Pure refactor - output unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jonaslagoni jonaslagoni requested a review from ALagoni97 as a code owner July 16, 2026 15:22
@vercel

vercel Bot commented Jul 16, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
the-codegen-project Ready Ready Preview, Comment Jul 16, 2026 3:37pm
the-codegen-project-mcp Ready Ready Preview, Comment Jul 16, 2026 3:37pm

@ALagoni97

Copy link
Copy Markdown
Contributor

I suggest maybe taking a look at this PR: #422. We want the same thing, but I do it slightly different. 😄

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants