Skip to content

tapped's gate is an arity error: the last rest tuple #83

Description

@btravers

@btravstack/testing's tapped is the last compile-time gate in this repo that
rides a conditional rest tuple — the shape #66 removed from start precisely
because an arity error never prints a type. #82 documented it honestly rather
than fixing it; this is the fix.

What it prints today

packages/testing/src/tapped.ts:34-36:

...gate: [Exclude<InstanceType<P[number]>, X>] extends [never]
  ? []
  : [error: "NOT EXPORTED", missing: Exclude<InstanceType<P[number]>, X>]

Tapping a port the module does not export (measured, scratch file in
packages/testing/src, npx tsc -p tsconfig.json --noEmit):

error TS2554: Expected 4 arguments, but got 2.

That is the whole message. Neither NOT EXPORTED nor the offending port
reaches the reader. Both are in the parameter's type and answer one slot at a
time, which is the technique /reference/di/entry-points#the-gate teaches:

error TS2345: Argument of type '0' is not assignable to parameter of type '"NOT EXPORTED"'.
error TS2345: Argument of type 'number' is not assignable to parameter of type 'Secret'.

Secret is the port — reachable in two hand-spelled steps, printed in none.

The fix, and the one thing that has to be measured

start moved its marker onto the module parameter and the sentence started
printing, because a parameter type is something TypeScript prints and an arity
count is not. The same move applies here, with one open question: which
parameter carries it.

  • ports reads best — the mistake is the ports array, and a sentence like
    "NOT EXPORTED — the module exports no port named Secret" lands on the
    argument that is wrong.
  • module is the closer analogue to start, and the safer inference bet.

The risk is stated in packages/core/src/start.ts:139: a conditional type in an
inference-bearing position can defer that parameter's inference and collapse a
type parameter to unknown. For start it was measured not to — X still
infers from the Module<X, …> half of the intersection. Here P is
const P extends readonly AnyPort[], inferred from ports, and a const type
parameter under an intersection is not obviously the same case. Measure both
placements before choosing
; do not reason it out.

Whether the port's name can reach the sentence is a second measurement.
@btravstack/http's UNDECLARED KEY gets its key in via a template literal
because its mapped type is keyed by K (#82, ac12553). tapped has an
Exclude<...> of port instances rather than a key, so the same trick may not
be available — if it is not, "NOT EXPORTED — this module exports no such port"
printed in full is still strictly better than an arity count.

What moves with it

  • packages/testing/src/tapped.ts — the gate, and its TSDoc, which currently
    says the gate "refuses a port module does not export, at this call site".
  • docs/reference/testing.md — the #the-tap-gate-an-arity-error section, in
    full: its anchor is its claim, so the anchor changes too, and every inbound
    link with it (docs/how-to/test-an-application.md is one).
  • docs/explanation/compile-time-wiring.md — "Three gate mechanisms live in
    this repo that a composing application meets… A fourth lives in the test
    harness". If tapped stops being a rest tuple, the fourth stops being an
    arity gate and that whole passage is restated.
  • A *.test-d.ts pinning the new arm. packages/testing has no type tests and
    no tsconfig.test-d.json today — packages/temporal was in exactly this
    position before Keyed dependency records for Provider — positional deps are the highest-frequency DX wart #65 and gained both, so there is a worked precedent.
  • A changeset. @btravstack/testing only; the nine move as one anyway.

Not in scope

di's own gate on Module.scoped / build / forkScope stays a rest tuple, and
that is a decision, not an oversight — docs/explanation/compile-time-wiring.md
argues it: the arity form points the blame at the call rather than deep inside a
generic instantiation, and di's missing: N is a set of ports a reader can
read off the signature, where the kernel's is one of three fixed sentences.
tapped's is a single sentence plus one port, which is the kernel's shape, not
di's.


Found by the whole-branch review on #82, which caught that #66's sweep had
stopped at three mechanisms when there were four.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Wanted, not urgentenhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions