Repository navigation
refactor(controller): type ComputedParameter.provider without method bivariance - #110
Merged
Merged
Conversation
…bivariance Store a provider that declares a RequestContext subtype through one explicit assertion in ControllerMeta.setProvider, with a SAFETY comment, instead of an indexed method type that hid the narrowing from strictFunctionTypes. Move the runtime refusal of a controller-less handler into a plain-JS spec so no @ts-expect-error is needed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
ANT-1111 aims to bring this repo's suppression budget to zero by fixing the code each suppression hides, so the lint holds without escape hatches. Two of Antony's decisions apply here.
ComputedParameter.providerwas typed{ provide(context: RequestContext): … }["provide"]. That is a method signature read through an indexed access. Method parameters are bivariant, so a provider that reads aRequestContextsubtype was stored with no error and no assertion. The narrowing was still there. It was just hidden fromstrictFunctionTypes. Antony chose to make it visible: one explicitasinControllerMeta.setProviderwith aSAFETY:comment, which raises thesafetyceiling from 0 to 1.RouteHandler.controllerto check the runtime refusal needed@ts-expect-error. Antony chose option E. The case moves to a plain-JS*.untyped.spec.jsthat calls the built module, and no@ts-expect-erroris left.What changes
src/controller.ts.ComputedParameter.provideris nowParameterProvider.setProviderstoresprovider as ParameterProviderunder aSAFETY:comment.src/tests/route-lifecycle.untyped.spec.jsis new. It holds the "refuses a handler that names no controller and registers nothing" case, with the same three observations as before: the TypeError message, an unchangedgetRegisteredRoutes()count, and no call on a recordingroutesProxyprovider. The case is removed fromroute-lifecycle.test.ts.tsconfig.jsonsetsallowJs: trueandcheckJs: false, so tsc copies the spec intodist/tests. The core runner matches/\.(test|spec)\.(js|ts)$/there. This is the same setup AntelopeJS/mongodb uses for its untyped specs.knip.config.tslistssrc/tests/**/*.untyped.spec.jsas an entry.guardrails.suppressions.jsonchangestsExpectErrorfrom 1 to 0 andsafetyfrom 0 to 1. Thesafetyraise is Antony's decision. Thesuppressionscheck reports it as a raise against the base until the ruleset bypass is applied.Attention
Published declaration change.
ComputedParameter.providerchanges type indist/index.d.ts(diff below). Code that assigns a provider reading aRequestContextsubtype directly toComputedParameter.providerno longer compiles. Such code goes throughSetParameterProvider, which is unchanged, or asserts at its own boundary. Runtime exports are unchanged. A caller that only readsprovidersees the same(context: RequestContext) => unknown.Ceiling raise.
safetygoes from 0 to 1, as Antony approved. Thesuppressionsstep of the checks fails on the raise against base until he applies the ruleset bypass. Every other check passes.SAFETY:comments addedsrc/controller.ts, inControllerMeta.setProvider. The comment reads: "Context names the slots that the caller's controller decorator or prefix handler sets on the request before this provider runs, and computeParameter passes that same request object, so the provider receives the Context it declared." No type-level fix exists. Providers of different contexts sit side by side inRecord<PropertyKey, ComputedParameter>. The context a route supplies is decided at run time, when a prefix handler or decorator writes a slot onto the request object. TypeScript has no existential type to keep each entry'sContextand relate it to the requestcomputeParameterreceives. A genericComputedParameter<Context>would still need the same widening where the record stores it, and it would change more of the published contract.Known cases left in place
anti-slop/no-unknown-parametersatsrc/controller.ts:110onprevious: unknownin the publishedParameterModifier. Each stacked decorator is a separate call, so no type carries what the step below produced. This waits on Antony's decision onunknownin published contract signatures.Proof
The moved test still catches a wrong implementation
Head,
pnpm exec ajs module test . -f dist/tests/route-lifecycle.test.js -f dist/tests/route-lifecycle.untyped.spec.js -f dist/tests/request-slot.test.js:Built
dist/routes.jswith the!handler.controllerguard replaced byfalse:Built
dist/routes.jscallingroutesProxy.register('leak', handler)before it throws:The narrowing is now visible to the type checker
The probe assigns a
ParameterProvider<string, TenantContext>straight toComputedParameter.provider, compiled against each side'ssrc/(onlyprobe.tslines shown).Before (origin/main): no
probe.tserror. The narrowing is accepted silently.After:
src/tests/request-slot.test.tsstill passes (above): aTenantContextprovider registered throughSetParameterProviderreads the slot a prefix handler set.API diff