Skip to content

fix(aws-lambda): infer response type from event via overloads#4872

Open
Danny-Devs wants to merge 2 commits intohonojs:mainfrom
Danny-Devs:fix/aws-lambda-handle-return-typing
Open

fix(aws-lambda): infer response type from event via overloads#4872
Danny-Devs wants to merge 2 commits intohonojs:mainfrom
Danny-Devs:fix/aws-lambda-handle-return-typing

Conversation

@Danny-Devs
Copy link
Copy Markdown

@Danny-Devs Danny-Devs commented Apr 12, 2026

Summary

While reviewing the AWS Lambda adapter, I noticed handle() used a conditional return type guarded by @ts-expect-error FIXME: Fix return typing. Tracing the FIXME back to commit 0e4dfe5 (#3883), the author's goal was to "Infer response type from event" — narrowing to WithHeaders for events that cannot produce multi-value headers.

The issue

The conditional L extends { multiValueHeaders: Record<string, string[]> } ? WithMultiValueHeaders : WithHeaders is vacuous. Every LambdaEvent member declares multiValueHeaders as optional (v1, ALB) or omits/undefines it (v2 which has ?: undefined, Lattice which has no such field), so none satisfy the required-field extends constraint. The conditional always resolves to WithHeaders regardless of the input event type, and the @ts-expect-error on the implementation hides the mismatch between the declared return type and the actual value produced by processor.createResult (which at runtime may return either branch, based on 'multiValueHeaders' in event && event.multiValueHeaders).

The fix

Replace the conditional with per-event overloads that encode the real runtime guarantee. The overloads are ordered for resolution:

  1. v2 / LatticeAPIGatewayProxyResult & WithHeaders — these event types never produce multi-value headers
  2. Any event with multiValueHeaders presentAPIGatewayProxyResult & WithMultiValueHeaders — the runtime uses multi-value headers when the event carries them
  3. Any event with multiValueHeaders absent/undefinedAPIGatewayProxyResult & WithHeaders — the runtime uses regular headers otherwise
  4. Generic LambdaEvent fallback → the full APIGatewayProxyResult union — for callers whose event type is not statically known

This models the exact runtime behavior of getProcessor and createResult, so callers get the most precise return type the type system can express from their call-site argument.

The overload pattern matches HonoRequest.param in src/request.ts:94-104. The as LambdaHandlerFunction cast is the idiomatic TypeScript pattern for giving a const arrow function multiple call signatures — arrow functions cannot use stacked function declaration overloads.

No change in runtime behavior. The @ts-expect-error FIXME: Fix return typing directive is removed.

Verification

  • bun tsc --build — clean (the runtime-tests/lambda test file compiles without changes, since the overloads are precise enough for TypeScript to infer the correct return type from each test's inline event literal)
  • bun run test — 4255 passed, 33 skipped, 0 failed
  • bun run build — green (includes declaration emission and publint)
  • bun run format:fix && bun run lint:fix — no changes needed
  • Type-level narrowing also verified locally with an @ts-expect-error-style test file that imported the real handle from this branch and asserted the narrowing contract for all four event types (v2/Lattice narrow to WithHeaders without a discriminant; v1/ALB correctly require one when multiValueHeaders presence is unknown). Happy to contribute that as a follow-up if the team would like a regression guard in the test suite — kept this PR focused on the fix itself.

The author should do the following, if applicable

  • Add tests (type-level verification done locally; see note above)
  • Run tests
  • bun run format:fix && bun run lint:fix to format the code
  • Add TSDoc/JSDoc to document the code (no new public API surface)

The handle() return type used a conditional
L extends { multiValueHeaders: Record<string, string[]> } that
was vacuous — every LambdaEvent member declares the field as
optional (v1, ALB) or omits/undefines it (v2, Lattice), so the
conditional always resolved to the WithHeaders branch regardless
of the input event type. A @ts-expect-error FIXME hid the
mismatch between the declared return type and the actual value
returned by processor.createResult.

Replace with per-event overloads that fulfill the "Infer response
type from event" intent from 0e4dfe5: v2 and Lattice narrow to
APIGatewayProxyResult & WithHeaders (runtime cannot produce the
multi-value branch for them); v1 and ALB return the full union.

The overload pattern matches HonoRequest.param in src/request.ts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@codecov
Copy link
Copy Markdown

codecov bot commented Apr 12, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.89%. Comparing base (1aa32fb) to head (8542290).

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #4872   +/-   ##
=======================================
  Coverage   92.89%   92.89%           
=======================================
  Files         177      177           
  Lines       11797    11801    +4     
  Branches     3515     3514    -1     
=======================================
+ Hits        10959    10963    +4     
  Misses        837      837           
  Partials        1        1           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

…sence

Refine LambdaHandlerFunction overloads so the return type reflects the
runtime's multiValueHeaders decision:

- Event with multiValueHeaders present  → WithMultiValueHeaders
- Event with multiValueHeaders absent   → WithHeaders
- Generic LambdaEvent (unknown)         → full union

This eliminates 12 TS18048 errors in the runtime-tests without any test
changes — the type system now models the exact runtime behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@Danny-Devs
Copy link
Copy Markdown
Author

The "Type & Bundle size Check on PR" failed on the first push — tsc --build caught 12 TS18048 errors in runtime-tests/lambda/index.test.ts where the test code accessed .headers or .multiValueHeaders without a check.

This was the original overloads being correct but imprecise: v1/ALB returned the full APIGatewayProxyResult union, so TypeScript correctly flagged .headers as possibly undefined. The test file was silently relying on the old (vacuous) type that always resolved to WithHeaders.

Rather than adding non-null assertions to the test file, I refined the overloads to also narrow based on multiValueHeaders presence in the event — this models the exact runtime behavior of getProcessor/createResult, so all 12 errors resolve without any test changes. Updated the PR description with details.

@yusukebe
Copy link
Copy Markdown
Member

@Danny-Devs

It's good to remove FIXME: Fix return typing, but is there any use case to use the type returned from handle?

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