Skip to content

fix(effect): default middlewareGen type parameters so standalone middleware works on concrete procedures - #1996

Merged
dinwwwh merged 2 commits into
middleapi:mainfrom
Mnigos:effect-middleware-gen-defaults
Sep 7, 2026
Merged

fix(effect): default middlewareGen type parameters so standalone middleware works on concrete procedures#1996
dinwwwh merged 2 commits into
middleapi:mainfrom
Mnigos:effect-middleware-gen-defaults

Conversation

@Mnigos

@Mnigos Mnigos commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

A standalone middleware created with middlewareGen cannot be attached to any procedure that has a concrete output:

const requireAuth = middlewareGen(function* ({ context, next }) { /* … */ })

os.$context<ServerContext>()
  .input(z.object({ id: z.string() }))
  .output(z.string())
  .use(requireAuth)
// ^ TOutput infers `unknown`, but procedure-level `.use()` requires the
//   procedure's output type — does not compile

Before this change, the only way to make it compile was to spell out all six type arguments, including a hand-written any:

const requireAuth = middlewareGen<
  ServerContext,
  unknown,
  any, // required just so `.use()` accepts the middleware on any output
  Record<never, never>,
  Effect.Effect<unknown, unknown, InferEffectServices<ServerContext>>,
  { user: User }
>(function* ({ context, next }) {
  if (!context.auth)
    return yield* Effect.fail(new ORPCError('UNAUTHORIZED'))

  return yield* next({ context: { user: context.auth.user } })
})

Core already solves this for vanilla middleware: Builder.middleware declares $Output = any with the comment "$Output = any by default is important to make middleware can be used in any output by default" (packages/server/src/builder.ts). middlewareGen has no defaults, so inference falls back to unknown and the middleware is only usable inline or on builders without input/output schemas — which is exactly what the existing standalone type test covered, so this went unnoticed.

This mirrors the core defaults (TInput = unknown, TOutput = any, TErrorMap = Record<never, never>, TYield defaulted) and moves TOutContext to the second position, so the same middleware becomes:

const requireAuth = middlewareGen<ServerContext, { user: User }>(
  function* ({ context, next }) {
    if (!context.auth)
      return yield* Effect.fail(new ORPCError('UNAUTHORIZED'))

    return yield* next({ context: { user: context.auth.user } })
  },
)

And when the middleware is created through a builder's .middleware(), no type arguments are needed at all — the in-context arrives contextually and the narrowed out-context is inferred from next({ context: … }):

const requireAuth = os
  .$context<ServerContext>()
  .middleware(middlewareGen(function* ({ context, next }) {
    if (!context.auth)
      return yield* Effect.fail(new ORPCError('UNAUTHORIZED'))

    return yield* next({ context: { user: context.auth.user } })
  }))

(The zero-argument form compiles under tsc even without the defaults thanks to context-sensitive inference threading $Output = any into the inner call, but the TypeScript 7 native compiler resolves the inner call to unknown on the current signature — with the defaults it works there too. Both forms are covered by the added type tests.)

Note the reorder is breaking for callers passing explicit type arguments — but passing all six explicitly was previously the only way to make standalone middleware compile, so the affected code is the code this fixes. If you'd rather avoid the reorder, adding only the defaults (keeping the current order) also fixes compilation; the reorder is what makes the explicit form pleasant.

Adds type tests attaching a standalone generator middleware to a procedure with concrete input/output — via .use() with explicit type arguments, and via .middleware() with none.

Validated against a real consumer (an Effect 4 + @orpc/experimental-effect API on TypeScript 7): the auth-guard middleware in the six-argument form above reduces to the zero/two-argument forms, with the app's full typecheck and test suite passing.

@Mnigos
Mnigos force-pushed the effect-middleware-gen-defaults branch from 5eb68c0 to 9ab2359 Compare September 7, 2026 10:41

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes

  • Default middlewareGen type parameterspackages/effect/src/middleware.ts gives TInput = unknown, TOutput = any, TErrorMap = Record<never, never>, and TYield defaults, and moves TOutContext to the second generic position, mirroring core's Builder.middleware ($Output = any) so a standalone generator middleware can be .used on procedures with concrete input/output.
  • Type test for standalone middleware on concrete proceduresmiddleware.test-d.ts attaches a standalone generator middleware to a concrete-input/output procedure via both .use() and os.middleware(...), asserting context flows through.

I verified pnpm --filter @orpc/experimental-effect type:check passes with the new test, and eslint is clean on both files. The test is genuine coverage: under the old signature middlewareGen<ServerContext, { user: 'user' }> bound the second arg to TInput and left TOutput/TErrorMap unsatisfied (plus TOutput = unknown would break .use regardless), so it fails on the old code.

One thing worth keeping in mind (already acknowledged in the PR description): the parameter reorder is not a compiler error for external callers who passed explicit type args in the old order — those get silently remapped positionally. Given only the previously-broken all-six-explicit form was affected and this is the experimental @orpc/experimental-effect package, that tradeoff is reasonable; a release-note mention is the only follow-up worth considering.

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes

  • Re-reviewed head at 9ab23591d — this is a force-pushed/amended commit whose diff is byte-identical to the previously reviewed 5eb68c0e3 (both changed files match exactly, same hunks, same line numbers). No new substantive changes; the earlier approval stands.

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes

  • Added zero-argument middlewareGen type testmiddleware.test-d.ts adds infers everything without type arguments when created via os.middleware, covering middlewareGen(...) with no type arguments inside os.$context<ServerContext>().middleware(...), then .used on a procedure with concrete input/output, asserting context narrowing flows through to the handler.

I verified the new test genuinely guards the fix rather than being decorative: with middleware.ts reverted to the old no-defaults signature, the root pnpm type:check (which is what exercises *.test-d.ts) fails at middleware.test-d.ts(126,12)TOutput/TInput collapse to unknown and .use(requireAuth) no longer assigns. So it is real regression coverage; that it also fails under this repo's tsc harness makes it stronger than the PR description implies (which attributes the failure only to a TypeScript 7 native compiler). Full type check and lint are clean at head.

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@pkg-pr-new

pkg-pr-new Bot commented Sep 7, 2026

Copy link
Copy Markdown
More templates

@orpc/ai-sdk

npm i https://pkg.pr.new/@orpc/ai-sdk@1996

@orpc/arktype

npm i https://pkg.pr.new/@orpc/arktype@1996

@orpc/bun

npm i https://pkg.pr.new/@orpc/bun@1996

@orpc/client

npm i https://pkg.pr.new/@orpc/client@1996

@orpc/cloudflare

npm i https://pkg.pr.new/@orpc/cloudflare@1996

@orpc/contract

npm i https://pkg.pr.new/@orpc/contract@1996

@orpc/experimental-effect

npm i https://pkg.pr.new/@orpc/experimental-effect@1996

@orpc/evlog

npm i https://pkg.pr.new/@orpc/evlog@1996

@orpc/hibernation

npm i https://pkg.pr.new/@orpc/hibernation@1996

@orpc/json-schema

npm i https://pkg.pr.new/@orpc/json-schema@1996

@orpc/experimental-msw

npm i https://pkg.pr.new/@orpc/experimental-msw@1996

@orpc/nest

npm i https://pkg.pr.new/@orpc/nest@1996

@orpc/next

npm i https://pkg.pr.new/@orpc/next@1996

@orpc/node

npm i https://pkg.pr.new/@orpc/node@1996

@orpc/openapi

npm i https://pkg.pr.new/@orpc/openapi@1996

@orpc/opentelemetry

npm i https://pkg.pr.new/@orpc/opentelemetry@1996

@orpc/pinia-colada

npm i https://pkg.pr.new/@orpc/pinia-colada@1996

@orpc/pino

npm i https://pkg.pr.new/@orpc/pino@1996

@orpc/publisher

npm i https://pkg.pr.new/@orpc/publisher@1996

@orpc/ratelimit

npm i https://pkg.pr.new/@orpc/ratelimit@1996

@orpc/server

npm i https://pkg.pr.new/@orpc/server@1996

@orpc/shared

npm i https://pkg.pr.new/@orpc/shared@1996

@orpc/swr

npm i https://pkg.pr.new/@orpc/swr@1996

@orpc/tanstack-query

npm i https://pkg.pr.new/@orpc/tanstack-query@1996

@orpc/trpc

npm i https://pkg.pr.new/@orpc/trpc@1996

@orpc/valibot

npm i https://pkg.pr.new/@orpc/valibot@1996

@orpc/zod

npm i https://pkg.pr.new/@orpc/zod@1996

commit: b821c82

@codecov

codecov Bot commented Sep 7, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@codspeed-hq

codspeed-hq Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 30 untouched benchmarks


Comparing Mnigos:effect-middleware-gen-defaults (b821c82) with main (a89c5a9)

Open in CodSpeed

@dinwwwh
dinwwwh merged commit d7f6569 into middleapi:main Sep 7, 2026
11 checks passed
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