Skip to content

feat: separate input/output types on AppType for Eden Treaty#1728

Closed
raunak-rpm wants to merge 4 commits intoelysiajs:mainfrom
raunak-rpm:feat/separate-input-output-types-1718
Closed

feat: separate input/output types on AppType for Eden Treaty#1728
raunak-rpm wants to merge 4 commits intoelysiajs:mainfrom
raunak-rpm:feat/separate-input-output-types-1718

Conversation

@raunak-rpm
Copy link

@raunak-rpm raunak-rpm commented Feb 9, 2026

Summary

Fixes #1718 — Adds separate input/output types on the AppType so Eden Treaty can differentiate between what the client sends (pre-transform) and what the handler receives (post-transform).

Problem

When using schemas with transforms (e.g., z.string().datetime().transform(v => new Date(v))), Elysia's AppType only exposes the output type (Date). Eden Treaty reads this type for client request payloads, but the client should send the input type (string).

Currently UnwrapSchema always resolves to ['output'] for Standard Schema and ['static'] (decoded) for TypeBox. CreateEdenResponse — the contract type Eden reads — only contains these output types.

Solution

New Type Utilities (src/types.ts)

  • UnwrapSchemaInput — Like UnwrapSchema but resolves Standard Schema to ['input'] instead of ['output']. TypeBox schemas resolve identically (input === output due to Elysia's cast pattern).
  • UnwrapBodySchemaInput — Body variant of UnwrapSchemaInput with null support for optional bodies.
  • UnwrapRouteInput — Like UnwrapRoute but uses input type utilities for body, headers, query, params. Response stays as output (it's what the server sends back).

Extended Eden Contract (CreateEdenResponse)

Added an optional SchemaInput type parameter (defaults to Schema for backward compatibility) and a new input property:

type Route = {
  // Existing (output types - backward compatible)
  body: { name: string; createdAt: Date }
  query: { page: number }
  headers: Record<string, string>
  response: { 200: string }

  // NEW: input types for client-side usage
  input: {
    body: { name: string; createdAt: string }    // pre-transform
    query: { page: string }                       // pre-transform
    headers: Record<string, string>
  }
}

Route Method Updates (src/index.ts)

All 11 HTTP method signatures (get, post, put, patch, delete, options, all, head, connect, route, ws) now compute SchemaInput via UnwrapRouteInput and pass it to CreateEdenResponse.

Backward Compatibility

  • Zero breaking changes: existing body, query, headers, params remain output types
  • Zero runtime changes: this is purely a type-level enhancement
  • When no transforms are used: input === output (no overhead)
  • New types are exported for external consumers (Eden Treaty)

Usage

import { Elysia } from 'elysia'
import z from 'zod'

const app = new Elysia().post('/users', () => 'ok', {
  body: z.object({
    name: z.string(),
    createdAt: z.string().transform(s => new Date(s))
  })
})

type Routes = typeof app['~Routes']
type Route = Routes['users']['post']

// Output type (what handler receives)
Route['body']           // { name: string; createdAt: Date }

// Input type (what client sends) - NEW
Route['input']['body']  // { name: string; createdAt: string }

Testing

  • Build: Passes successfully
  • Runtime: 1500/1500 tests pass, zero regressions
  • Type-level: 11 comprehensive type tests with expect-type covering:
    • TypeBox schemas (input === output)
    • Zod schemas without transforms (input === output)
    • Zod transforms on body, query, headers
    • Mixed transformed/non-transformed fields
    • Multiple HTTP methods
    • Explicit params schemas
    • Response types unaffected
    • Plugin composition
    • Backward compatibility verification
  • TSC errors: Reduced from 65 to 58 (no new errors introduced)

Summary by CodeRabbit

  • New Features

    • Exposes input (pre-transform) type information for routes and schemas, adding public input-focused types (UnwrapRouteInput, UnwrapSchemaInput, UnwrapBodySchemaInput, MacroToContextInput) and extending response typings to include input shapes alongside outputs.
  • Tests

    • Adds extensive type-level and runtime tests validating input/output separation, transforms, coercions, defaults, nested schemas, plugin composition, lifecycle/route combos, and metadata for input shapes.

…lysiajs#1718)

- Add UnwrapSchemaInput type: resolves Standard Schema to input type
  (pre-transform) instead of output type (post-transform)
- Add UnwrapBodySchemaInput type: body variant with null support
- Add UnwrapRouteInput interface: resolves route to input types for
  body, headers, query, params; response stays as output
- Extend CreateEdenResponse with optional SchemaInput parameter and
  new 'input' property containing pre-transform types
- Thread SchemaInput through all 11 HTTP method signatures
- Export new type utilities for external consumers (Eden Treaty)
- Add 11 comprehensive type-level tests with Zod transforms

When no transforms are used, input === output (zero overhead).
For Zod/Valibot transforms, input reflects the raw shape clients send.
Fully backward compatible: existing body/query/etc remain output types.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 9, 2026

Walkthrough

Adds input-focused type utilities and propagates input generics through route/macro signatures so route types expose pre-transform (input) shapes alongside existing output types; updates public exports and adds extensive type- and runtime-tests validating input vs output separation.

Changes

Cohort / File(s) Summary
Index exports
src/index.ts
Exported new public types: UnwrapRouteInput, UnwrapSchemaInput, UnwrapBodySchemaInput, MacroToContextInput and added SchemaInput generics to multiple route/macro signatures (get, post, put, patch, delete, options, all, head, connect, route, ws).
Core types
src/types.ts
Added input-resolution utilities (UnwrapSchemaInput, UnwrapBodySchemaInput, UnwrapRouteInput, UnwrapMacroSchemaInput, MacroToContextInput) and extended CreateEdenResponse with SchemaInput/MacroContextInput generics and an input field mirroring body/params/query/headers.
Type tests (standard & deep)
test/types/standard-schema/input-output.ts, test/types/standard-schema/input-output-deep.ts
Added extensive type-level tests asserting separation of pre-transform (input) vs post-transform (output) across Zod, TypeBox, mixed schemas, nested structures, plugins, guards, macros, and many combinations.
Runtime tests
test/core/input-output-runtime.test.ts
Added runtime tests exercising Zod transforms/coercions/defaults and asserting transformed runtime behavior and presence of input metadata on exported routes.
Test route typings & lifecycle
test/types/index.ts, test/types/lifecycle/soundness.ts
Updated generated route/type snapshots and lifecycle tests to include new input shapes for routes (body, params, query, headers).

Sequence Diagram(s)

(omitted)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Elysia 1.4: Weirs #1398: Modifies similar type-level public surfaces (UnwrapRouteInput/UnwrapSchemaInput and CreateEdenResponse input generics).
  • 1.4.5 patch #1403: Alters route/macro signatures and exported types that overlap with the new input-typing changes.
  • patch 1.4.19 #1610: Adjusts exported route/group generics and related public type signatures; relevant to these generics changes.

Poem

🐇 I nudged the schema, split the seed and rind,

Input for senders, output for bind,
Pre-transform carrots for clients to sow,
Handlers harvest the shapes they know,
Hooray — two baskets, neatly aligned!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: separate input/output types on AppType for Eden Treaty' clearly summarizes the main change: adding type-level separation between input and output types for the Eden Treaty integration.
Linked Issues check ✅ Passed All primary objectives from issue #1718 are met: input/output type separation is implemented, new type utilities expose both pre-transform and post-transform types, Eden Treaty can now distinguish client input from handler output, and backward compatibility is preserved.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing input/output type separation: type utilities (UnwrapSchemaInput, UnwrapRouteInput), CreateEdenResponse enhancements, HTTP method signature updates, and comprehensive type-level tests align with the stated objectives.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

No actionable comments were generated in the recent review. 🎉


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/types.ts (1)

2767-2823: ⚠️ Potential issue | 🟠 Major

Macro-defined transforms still leak output types into Route['input'].

CreateEdenResponse['input'] intersects SchemaInput with MacroContext, but MacroContext is derived via UnwrapRoute (output). If a macro contributes a Standard Schema with .transform(), the client-facing input.* types will still reflect the post-transform shape.

Consider introducing an input-oriented macro context (e.g., MacroToContextInput using UnwrapRouteInput) and threading it into CreateEdenResponse.

💡 Possible fix sketch
-export type CreateEdenResponse<
+export type CreateEdenResponse<
 	Path extends string,
 	Schema extends RouteSchema,
 	MacroContext extends RouteSchema,
 	Res extends PossibleResponse,
-	SchemaInput extends RouteSchema = Schema
+	SchemaInput extends RouteSchema = Schema,
+	MacroContextInput extends RouteSchema = MacroContext
 > = RouteSchema extends MacroContext
 	? {
 			body: Schema['body']
 			params: IsNever<keyof Schema['params']> extends true
 				? ResolvePath<Path>
 				: Schema['params']
 			query: Schema['query']
 			headers: Schema['headers']
 			response: Prettify<Res>
 			input: {
-				body: SchemaInput['body']
+				body: SchemaInput['body']
 				params: IsNever<keyof SchemaInput['params']> extends true
 					? ResolvePath<Path>
 					: SchemaInput['params']
 				query: SchemaInput['query']
 				headers: SchemaInput['headers']
 			}
 		}
 	: {
 			body: Prettify<Schema['body'] & MacroContext['body']>
 			params: IsNever<
 				keyof (Schema['params'] & MacroContext['params'])
 			> extends true
 				? ResolvePath<Path>
 				: Prettify<Schema['params'] & MacroContext['params']>
 			query: Prettify<Schema['query'] & MacroContext['query']>
 			headers: Prettify<Schema['headers'] & MacroContext['headers']>
 			response: Prettify<Res>
 			input: {
-				body: Prettify<SchemaInput['body'] & MacroContext['body']>
+				body: Prettify<SchemaInput['body'] & MacroContextInput['body']>
 				params: IsNever<
-					keyof (SchemaInput['params'] & MacroContext['params'])
+					keyof (SchemaInput['params'] & MacroContextInput['params'])
 				> extends true
 					? ResolvePath<Path>
-					: Prettify<SchemaInput['params'] & MacroContext['params']>
-				query: Prettify<SchemaInput['query'] & MacroContext['query']>
-				headers: Prettify<SchemaInput['headers'] & MacroContext['headers']>
+					: Prettify<SchemaInput['params'] & MacroContextInput['params']>
+				query: Prettify<SchemaInput['query'] & MacroContextInput['query']>
+				headers: Prettify<SchemaInput['headers'] & MacroContextInput['headers']>
 			}
 		}

- Fix 9 broken existing type tests (add input property to assertions)
- Add 28 deep type-level edge case tests (coerce, default, optional,
  chained transforms, nested, arrays, unions, guards, all HTTP methods,
  plugins, mixed TypeBox+Zod, Eden contract, TypeBox transforms)
- Add 9 runtime tests verifying transforms work correctly
- TSC: 0 errors (down from 58), Tests: 1509 pass, 0 fail
@pkg-pr-new
Copy link

pkg-pr-new bot commented Feb 9, 2026

Open in StackBlitz

npm i https://pkg.pr.new/elysiajs/elysia@1728

commit: d3ca179

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/index.ts`:
- Around line 7105-7109: The WS helper's MacroToContextInput generic is missing
the Definitions['typebox'] parameter, causing TypeBox ref resolution to differ
from HTTP helpers; update the MacroToContextInput usage near SchemaInput so it
becomes MacroToContextInput<Definitions['typebox'], Metadata['macroFn'],
Omit<Input, NonResolvableMacroKey>> (i.e., add Definitions['typebox'] as the
first generic type argument) so WS macro input types are consistent with other
HTTP method helpers.
🧹 Nitpick comments (1)
src/types.ts (1)

497-544: Consider a generic parameterized approach to reduce type duplication.

There are now effectively four pairs of near-identical types (UnwrapSchema/UnwrapSchemaInput, UnwrapBodySchema/UnwrapBodySchemaInput, UnwrapMacroSchema/UnwrapMacroSchemaInput, MacroToContext/MacroToContextInput) that differ only in whether they resolve ['input'] vs ['output'] on Standard Schema. This is ~200 lines of duplicated logic.

A possible approach is to introduce a discriminator generic (e.g., type SchemaMode = 'input' | 'output') and unify each pair into a single parameterized type:

// Sketch – single source of truth for both modes
type UnwrapSchemaMode<
  Mode extends 'input' | 'output',
  Schema extends AnySchema | string | undefined,
  Definitions extends DefinitionBase['typebox'] = {}
> = Schema extends undefined
  ? unknown
  : Schema extends TSchema
    ? /* ...TSchema branch (identical for both modes)... */
    : Schema extends FastStandardSchemaV1Like
      ? NonNullable<Schema['~standard']['types']>[Mode]
      : /* ...string branch with [Mode]... */

// Aliases for ergonomics / backward compat
export type UnwrapSchema<S, D> = UnwrapSchemaMode<'output', S, D>
export type UnwrapSchemaInput<S, D> = UnwrapSchemaMode<'input', S, D>

This would halve the surface area and ensure both modes stay in sync when the TSchema / string branches are updated. That said, I understand the trade-off with readability in complex conditional types, so this can be deferred.

Also applies to: 587-629, 1255-1350

@SaltyAom
Copy link
Member

This user has mass usage of fully automatic AI-generated pull requests without human interaction (also known as "AI slop”) to abuse issues and pull requests for multiple times for "karma farming" purpose

For example:

This is against CONTRIBUTING.md as stated

  • AI generated pull request without human interaction, review and supervision may result in close without further notice or ban from future contribution to Elysia

We have blocked this users to prevent them abusing the system in the future

Thank you for your understanding

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.

separate input/output on AppType

3 participants