Repository navigation
ts: a call through an annotated variable whose type needs the checker resolves via the initializer, disagreeing with getResolvedSignature at 9 measured sites #536
Description
Mechanism
When a call's callee is an identifier bound to a variable with a type annotation, the
parser resolves through the variable's initializer. tsc resolves through its
annotation. Where the annotation declares call signatures, the two answers differ.
Two shapes found, same cause.
1. A type literal with several call signatures, packages/microservices/decorators/message-pattern.decorator.ts:
export const MessagePattern: {
<T = PatternMetadata | string>(metadata?: T): MethodDecorator; // 28
<T = ...>(metadata?: T, transport?: Transport | symbol): MethodDecorator; // 29
<T = ...>(metadata?: T, extras?: Record<string, any>): MethodDecorator; // 33
<T = ...>(metadata?: T, transport?: ..., extras?: ...): MethodDecorator; // 37
} = <T = ...>(metadata?, transportOrExtras?, maybeExtras?): MethodDecorator => { ... }; // 42The parser already emits all four signatures correctly:
28:3 role=AMBIENT ovl=0 sig=<call-signature>(metadata)
29:3 role=AMBIENT ovl=1 sig=<call-signature>(metadata, transport)
33:3 role=AMBIENT ovl=2 sig=<call-signature>(metadata, extras)
37:3 role=AMBIENT ovl=3 sig=<call-signature>(metadata, transport, extras)
42:5 role=IMPLEMENTATION ovl=0 sig=<arrow>(metadata, transportOrExtras, maybeExtras)
For a two-argument call tsc picks the signature at 29. The parser picks 42, the arrow.
2. An indexed access through a generic alias, <subject>/src/middleware/devtools.ts:
const setStateFromDevtools: StoreApi<S>['setState'] = (...a) => { ... } // 258StoreApi<S>['setState'] is SetStateInternal<T>, itself { _(…): void; _(…): void }['_']
(src/vanilla.ts:1-7), an overload pair reached through two indexed accesses and a generic
instantiation. tsc picks vanilla.ts:2. The parser picks devtools.ts:258.
The part that is unambiguously wrong
At all twelve sites the parser emits:
ovlIdx=[] candCount=1 isOvlResolved=false evidence=LOCAL_BINDING targetKind=PROJECT_IMPLEMENTATION
overloadCandidateCount = 1 asserts there is exactly one candidate where tsc sees four (or
two), and isOverloadResolved = false asserts no choice was needed. A consumer reading those
concludes overload selection is unnecessary and never revisits the site. That is a wrong
number rather than a missing one, independent of which target is preferred.
It also contradicts this repo's own stated contract, the suite check "overloads never name
the implementation, a call resolves to an overload signature, and a shape member with two
signatures is not SOLE".
Measurement
Four projects with a root tsconfig, adjudicated against getResolvedSignature. Only sites
where the parser claimed a resolvedSignatureLinkHash are counted, joined on the
ts_expression span:
project claimed joined agree disagree
axios 78 78 78 0
<subject> 236 137 137 0
<subject> 160 160 151 9
nest 1998 1361 1358 3
------------------------------------------
TOTAL 1736 1724 12 agreement 99.31%, disagreement 0.69%
All 12 are this one construct. Where the parser resolves at all it is otherwise exactly right.
The construct's reach, counted from the parser's own rows (shapes owning ≥2 <call-signature>
members):
<subject> 24 shapes / 87 signatures <subject> 9/18 <subject> 8/20
axios 2/4 <subject> 2/13 <subject> 2/4 nest 2/12 <subject> 0 <subject> 0
49 shapes, 158 signatures corpus-wide. The reactive-streams library and the RPC framework could not be adjudicated, the reactive-streams library has no
root tsconfig.json and the oracle declines it, so 12 is a floor, not a total.
Why this is an issue and not a PR
Shape 2 cannot be resolved from syntax. Getting from StoreApi<S>['setState'] to the two
_ signatures in another file requires evaluating an indexed access on a generic
instantiation of a type alias. That is a TypeChecker computation, and the parser is
forbidden one, flagging it rather than quietly adding the dependency.
Shape 1 can, and cheaply: the annotation is a TypeLiteral whose CallSignatureDeclaration
members are already emitted as ts_method rows. Selecting among them by argument count is
syntactic, and is what the parser already does for function overloads (GrpcMethod in the
same file resolves correctly, with role=OVERLOAD_SIGNATURE).
But whether to change the target at all is a contract question, and it cuts both ways.
tsc's answer for shape 2 is vanilla.ts:2, a bodiless signature. The parser's answer is
the arrow that actually runs. For reachability the parser's target is arguably the more useful
one, and "resolve to the signature" would trade a body for a declaration. So there are three
defensible positions and picking among them is not mine:
- Resolve to the annotation's signature where syntax can find it (shape 1), matching tsc
and the existing suite contract; leave shape 2 unresolved. - Keep the implementation as the target, it is the callable body, but fix
overloadCandidateCount/isOverloadResolved/resolvedOverloadIndexso the row stops
claiming a single candidate, and setresolvedTargetKind = PROJECT_IMPLEMENTATION
deliberately rather than incidentally. - Emit both, the signature as the resolved target and the implementation via a separate
column, which is a schema change.
Option 2 is the smallest change that removes the wrong number without deciding the target
question, and it is the only one of the three that needs no schema edit and no checker.
Flagging for @ts-oracle / human ruling.
Acceptance
Discriminates today: on nest, three MessagePattern sites report candCount=1 against four
emitted AMBIENT <call-signature> rows for the same owner; on the state-management library, nine
setStateFromDevtools sites do the same against two. A check asserting
overloadCandidateCount >= count(sibling call signatures of the resolved target's owner)
fails on all twelve and passes elsewhere.
The fixture corpus has <call-signature> members but no call through a variable annotated
by a multi-signature shape, so all 34 checks pass either way. Raising the fixture with
ts-fixtures.
Activity
[-]ts: a call through an annotated variable resolves via the initializer, so a multi-signature shape reports overloadCandidateCount=1 and names the implementation[/-][+]ts: a call through an annotated variable whose type needs the checker resolves via the initializer, disagreeing with getResolvedSignature at 9 measured sites[/+]ts: a call through an annotation the parser cannot read stays unresol…
swapnilpaliwal-sd commentedon Sep 28, 2026
Fixed by #1636 (merged into 0.1.5).