Skip to content

Conversation

@MarcelOlsen
Copy link
Contributor

@MarcelOlsen MarcelOlsen commented Nov 15, 2025

Fixes type inference for response schemas and adds excess property checking

What was broken

Type inference for ~Routes was completely busted in 1.4.14+. Instead of getting the actual schema types, everything just resolved to unknown:

const app = new Elysia().get('/', () => ({ message: 'Hello Elysia' }), {
  response: {
    200: t.Object({
      message: t.Literal('Hello Elysia')
    })
  }
})

type Response = typeof app['~Routes']['get']['response']
// Expected: { 200: { message: 'Hello Elysia' }; 422: {...} }
// Got:      { 200: unknown; 422: unknown }

Also, TypeScript wasn't catching excess properties in status() calls:

// This was incorrectly allowed:
status(200, {
  message: "Hello Elysia",
  data: { idk: "test" }  // Not in schema, but no error
})

The fix

1. Fixed the backwards conditional in UnwrapSchema

Line 455 in src/types.ts had the check backwards. Changed undefined extends Schema to Schema extends undefined. The original check would always be true for union types since Schema extends AnySchema | string | undefined means undefined is potentially in the union.

2. Added excess property checking

Added a CheckExcessProps<T, U> type that enforces exact property matching. It distributes over union types correctly and still allows as any as an escape hatch when you need it.

type CheckExcessProps<T, U> = 0 extends 1 & T
  ? T // Allow any
  : U extends U // Distribute over unions
    ? Exclude<keyof T, keyof U> extends never
      ? T
      : { [K in keyof U]: U[K] } & { [K in Exclude<keyof T, keyof U>]: never }
    : never

The intersection with excess keys as never gives much better error messages - TypeScript points directly at the problem property instead of complaining about the whole object.

Applied this to SelectiveStatus in src/error.ts and the status function in src/context.ts.

What works now

  • Status code 200 and all other codes properly show their schema types
  • Macro response schemas merge correctly
  • Both TypeBox and Zod schemas work
  • Excess properties get caught with helpful error messages
  • Union type schemas work (the check distributes properly)
  • as any still works when you need to bypass checks

Better error messages

Before:

Argument of type '{ message: string; data: { idk: string; }; }' is not assignable to parameter of type 'never'.

After:

Type '{ idk: string; }' is not assignable to type 'never'.
     ^^^^

Points directly at the excess property instead of the whole object.

Tests

Here's what the type inference looks like now:

// Status code 200 inference
const app1 = new Elysia().get('/', () => ({ message: 'Hello' }), {
  response: { 200: t.Object({ message: t.Literal('Hello Elysia') }) }
})
type T1 = typeof app1['~Routes']['get']['response']
// { 200: { message: "Hello Elysia" }; 422: {...} }

// Macro response inference
const app2 = new Elysia()
  .macro({
    auth: {
      response: { 401: t.Literal('Unauthorized') },
      beforeHandle({ status }) {
        if (Math.random() < 0.05) return status(410)
      }
    }
  })
  .get('/', () => 'Hello', { auth: true })

type T2 = typeof app2['~Routes']['get']['response']
// { 401: "Unauthorized"; 410: "Gone"; 422: {...} }

// Excess property checking
status(200, { message: "Hello", extra: "prop" })
// Error on 'extra' property

Resolves #1548

@coderabbitai
Copy link

coderabbitai bot commented Nov 15, 2025

Walkthrough

Added a CheckExcessProps type and tightened several response-typing generics: SelectiveStatus signature updated to accept an explicit T generic; ErrorContext.status uses CheckExcessProps for excess-property checking; UnwrapSchema conditional reversed; type-only tests adjusted for 200-response inference.

Changes

Cohort / File(s) Summary
SelectiveStatus / error typing
src/error.ts
Updated SelectiveStatus<in out Res> to add an explicit generic T extends ... and make the response parameter T, returning ElysiaCustomStatusResponse<Code, T> (decouples response inference from Code).
ErrorContext / excess-prop checks
src/context.ts
Added CheckExcessProps<T, U> and changed ErrorContext.status to accept response: CheckExcessProps<...> while keeping return type ElysiaCustomStatusResponse<Code, T>.
Schema unwrap behavior
src/types.ts
Reversed top-level conditional in UnwrapSchema so Schema extends undefined is checked first, altering undefined handling and downstream optional-field inference.
Type tests
test/types/index.ts
Reworked imports and test callbacks, added/adjusted TypeScript tests validating 200 and multi-status (200/422) response type inference and replaced broad ignores with targeted checks.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Pay special attention to:
    • The exact constraint of the new T generic in SelectiveStatus and whether it fully mirrors prior conditional resolution.
    • Correctness of CheckExcessProps semantics — ensure it allows valid exact matches while rejecting excess properties.
    • The UnwrapSchema conditional change and its impact on optional/undefined inference across schemas.
    • Updated type tests (test/types/index.ts) for flakiness or false positives.

Possibly related PRs

  • patch: 1.4.16 #1529 — Modifies SelectiveStatus in src/error.ts with an explicit generic and response typing changes; strongly related.

Poem

🐰 I hopped through types both short and long,
Tweaked T and keys to right what's wrong,
Checked excess, unwrapped a schema's sigh,
Now 200 smiles and types comply. ✨

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title specifically references fixing 200 status code type inference, which directly aligns with the PR's primary objective of resolving issue #1548 about type inference regression for status code 200.
Linked Issues check ✅ Passed The PR addresses all coding requirements from issue #1548: reintroduces the missing T generic parameter to SelectiveStatus to restore type inference for 200 status codes, adds type-level tests validating inference, and restores pre-1.4.14 behavior without breaking changes.
Out of Scope Changes check ✅ Passed All changes are scoped to fixing type inference for status code 200: updates SelectiveStatus in error.ts and context.ts with generic parameters, modifies CheckExcessProps for excess property checking, adjusts UnwrapSchema conditionals, and adds validation tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

@MarcelOlsen MarcelOlsen changed the title Fix/200 type inference zod 🔧 Fix 200 status code type inference in ~Routes Nov 15, 2025
@pkg-pr-new
Copy link

pkg-pr-new bot commented Nov 15, 2025

Open in StackBlitz

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

commit: a7d4a8f

@MarcelOlsen
Copy link
Contributor Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Nov 15, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@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: 0

🧹 Nitpick comments (1)
test/types/index.ts (1)

2876-2929: Strengthen the 200-status type tests and align with existing style

The _typeTest / _typeTest2 conditional checks will still pass even if the inferred 200 response type is widened to any (e.g. via an index signature), so they don’t reliably guard against the regression you’re targeting. They also diverge from the rest of this file, which consistently uses expectTypeOf.

Consider asserting the concrete 200/422 payload types directly via expectTypeOf, and optionally updating the comment to reference issue #1548 instead of #200 for clarity. For example:

-// Status code 200 type inference (issue #200)
+// Status code 200 type inference (issue #1548)
 {
 	const app = new Elysia().get(
 		'/',
 		() => ({ message: 'Hello Elysia' as const }),
 		{
 			response: {
 					message: t.Literal('Hello Elysia')
 				})
 			}
 		}
 	)
 
-	type AppResponse = (typeof app)['~Routes']['get']['response']
-
-	// Should properly infer the 200 response type, not [x: string]: any
-	const _typeTest: AppResponse extends {
-		200: { message: 'Hello Elysia' }
-	}
-		? true
-		: false = true
+	type AppResponse = (typeof app)['~Routes']['get']['response']
+
+	// Ensure 200 response is the precise payload type, not widened (`any`)
+	expectTypeOf<AppResponse[200]>().toEqualTypeOf<{
+		message: 'Hello Elysia'
+	}>()
 
 	// Test with multiple status codes including 200
 	const app2 = new Elysia().post(
@@ -2901,15 +2899,17 @@
 			}
 		}
 	)
 
 	type App2Response = (typeof app2)['~Routes']['test']['post']['response']
 
-	const _typeTest2: App2Response extends {
-		200: { message: 'Hello Elysia' }
-		422: { error: string }
-	}
-		? true
-		: false = true
+	expectTypeOf<App2Response[200]>().toEqualTypeOf<{
+		message: 'Hello Elysia'
+	}>()
+
+	expectTypeOf<App2Response[422]>().toEqualTypeOf<{
+		error: string
+	}>()
 }

This way the tests will fail if 200/422 ever regress to any again, and they stay consistent with the rest of the type suite.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 78f4828 and 84c846f.

📒 Files selected for processing (2)
  • src/error.ts (1 hunks)
  • test/types/index.ts (1 hunks)
🔇 Additional comments (1)
src/error.ts (1)

45-55: SelectiveStatus generic reintroduction looks correct; consider factoring the constraint

Reintroducing a separate T generic tied to Res via the Code-indexed conditional restores the relationship between the response argument and ElysiaCustomStatusResponse<Code, T> and should fix the 200-inference regression as intended. The constraint logic itself matches the previous inline conditional.

If you want to improve readability, you could factor the conditional into a helper alias (e.g. type ResponseForCode<Res, Code> = ...) and reuse it here and anywhere else this mapping is needed, but that’s optional.

@Vicentesan
Copy link

hey @MarcelOlsen, tysm for reviewing the report so quickly. i really appreciate it. however, this PR seems to introduce an old error that was fixed in version 1.4.14. could you please take a look at it as well? thanks!

@MarcelOlsen
Copy link
Contributor Author

MarcelOlsen commented Nov 15, 2025

Oh, thanks for catching this! I'll look into this asap. Tests passed, so I assumed I'm good

@MarcelOlsen
Copy link
Contributor Author

Hey @Vicentesan

I investigated the concern about reintroducing the #1510 bug, and I can confirm the fix maintains strict status checking. Here's why:

The current implementation uses T extends ResponseType. While extends normally allows subtypes, when combined with:

  1. TypeScript's excess property checking for object literals
  2. Literal types (like t.Literal(true))

it actually does reject extra properties.

The proof is in the tests

The existing test suite (from the #1510 fix) still passes and catches the exact scenario you mentioned:

// Strict status response (test/types/index.ts:2843-2875)
new Elysia().post('/mirror', async ({ status, body }) => {
  // @ts-ignore - allows body property (wrong value type)
  return status(201, { body, success: false })

  // @ts-expect-error - correctly rejects wrong literal value
  return status(200, { success: false })  // Expected: true

  // @ts-expect-error - correctly rejects wrong literal value
  return status(201, { success: true })  // Expected: false
}, {
  response: {
    200: t.Object({ success: t.Literal(true) }),
    201: t.Object({ success: t.Literal(false) })
  }
})

All type tests pass, confirming strict checking works. The @ts-expect-error annotations prove TypeScript correctly rejects responses that don't match the schema.

Both issues are now fixed

Let me know if I'm missing somehting

@MarcelOlsen MarcelOlsen marked this pull request as ready for review November 15, 2025 16:56
@Vicentesan
Copy link

Hey @Vicentesan

I investigated the concern about reintroducing the #1510 bug, and I can confirm the fix maintains strict status checking. Here's why:

The current implementation uses T extends ResponseType. While extends normally allows subtypes, when combined with:

  1. TypeScript's excess property checking for object literals
  2. Literal types (like t.Literal(true))

it actually does reject extra properties.

The proof is in the tests

The existing test suite (from the #1510 fix) still passes and catches the exact scenario you mentioned:

// Strict status response (test/types/index.ts:2843-2875)
new Elysia().post('/mirror', async ({ status, body }) => {
  // @ts-ignore - allows body property (wrong value type)
  return status(201, { body, success: false })

  // @ts-expect-error - correctly rejects wrong literal value
  return status(200, { success: false })  // Expected: true

  // @ts-expect-error - correctly rejects wrong literal value
  return status(201, { success: true })  // Expected: false
}, {
  response: {
    200: t.Object({ success: t.Literal(true) }),
    201: t.Object({ success: t.Literal(false) })
  }
})

All type tests pass, confirming strict checking works. The @ts-expect-error annotations prove TypeScript correctly rejects responses that don't match the schema.

Both issues are now fixed

Let me know if I'm missing somehting

i checked in my IDE with your pkg-pr version, and it allowed more properties in the response schema defined by zod than expected. i will try again and let you know

@Vicentesan
Copy link

Vicentesan commented Nov 15, 2025

heres my test: https://github.com/Vicentesan/elysia-zod-response-type-issue/tree/trying-possible-fix

the AppResponse type is correct, but the status function is allowing additional properties

@MarcelOlsen
Copy link
Contributor Author

the example has elysia@latest added as a dep. I've installed the PR version, and everything seems to be working fine.

Type inference for AppResponse:
image

Strict property checking:
image

Make sure you clear node_modules when trying on a project, which has elysia added as a dep at any point.

@Vicentesan
Copy link

the example has elysia@latest added as a dep. I've installed the PR version, and everything seems to be working fine.

Type inference for AppResponse: image

Strict property checking: image

Make sure you clear node_modules when trying on a project, which has elysia added as a dep at any point.

it actually does not have the latest elysia version. you are in the wrong branch. the link i sent you is in the correct branch

just fyi, it throws an error if i add an additional property to the response schema that is missing in the status function. but, if i add an additional property to the status function, it breaks, as i mentioned earlier

Copy link

@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

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 84c846f and ce9490c.

📒 Files selected for processing (2)
  • src/error.ts (1 hunks)
  • test/types/index.ts (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
test/types/index.ts (2)
src/index.ts (4)
  • Elysia (185-8122)
  • Elysia (8124-8124)
  • t (8126-8126)
  • status (8167-8167)
src/error.ts (1)
  • status (94-102)
src/error.ts (1)
src/utils.ts (2)
  • StatusMap (560-622)
  • StatusMap (630-630)
🔇 Additional comments (2)
src/error.ts (1)

42-63: LGTM! Type signature correctly restores missing generic parameter.

The explicit generic parameter T for the response type properly restores the relationship between the response schema and the returned ElysiaCustomStatusResponse type. This change should fix the regression where status code 200 response types were incorrectly widened to index signatures.

The conditional type logic correctly resolves the response type from either Res[Code] or Res[StatusMap[Code]], maintaining consistency between the response parameter constraint and the return type.

test/types/index.ts (1)

2849-2850: LGTM! Improved test clarity with explicit error expectation.

The change from a generic suppression to @ts-expect-error with a clear explanation ("should reject extra 'body' property") makes the test intent explicit. This validates that TypeScript correctly enforces strict property checking against the response schema.

@MarcelOlsen MarcelOlsen marked this pull request as draft November 15, 2025 21:11
@MarcelOlsen
Copy link
Contributor Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Nov 16, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@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: 0

🧹 Nitpick comments (1)
src/context.ts (1)

21-27: Consider hoisting CheckExcessProps to a shared helper.

The same helper now lives here and in src/error.ts; moving it to a common spot (e.g., src/types.ts) would keep future tweaks in one place and avoid divergence.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ce9490c and a7d4a8f.

📒 Files selected for processing (4)
  • src/context.ts (2 hunks)
  • src/error.ts (1 hunks)
  • src/types.ts (1 hunks)
  • test/types/index.ts (7 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/types/index.ts
🔇 Additional comments (2)
src/types.ts (1)

455-456: LGTM – preserves schema inference.

Switching the guard to Schema extends undefined keeps optional unions from collapsing wholesale to unknown while still defaulting the pure undefined case, maintaining the intended unwrap behaviour.

src/error.ts (1)

50-75: Glad to see the generic restored.

Reintroducing T and piping it through both the parameter guard and ElysiaCustomStatusResponse re-establishes the response inference that regressed after the refactor.

@MarcelOlsen MarcelOlsen marked this pull request as ready for review November 16, 2025 16:39
@MarcelOlsen
Copy link
Contributor Author

Hey, @Vicentesan I think I misunderstood your original message, my bad. I think this should resolve the issue, my IDE is highlighting the additional data property:
image

@Vicentesan
Copy link

Hey, @Vicentesan I think I misunderstood your original message, my bad. I think this should resolve the issue, my IDE is highlighting the additional data property: image

just tried, now seems to be working! tysm

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.

Status Code 200 Breaks Type Inference in ~Routes with Zod

2 participants