Skip to content

TypeScript/JavaScript: local-variable receiver-type inference doesn't cover typed function parameters (unlike Java/C#/Kotlin/Swift/Scala) #1125

Description

@inth3shadows

Summary

The local-variable receiver-type inference added in #1108/#1110 covers typed parameters for every supported language except TypeScript/JavaScript (and TSX/JSX, which share the same pattern set). A method call through a typed function parameter — function use(lg: Logger) { lg.log(); } — never resolves via type inference in TS/JS, unlike the identical shape in Java, C#, Kotlin, Swift, and Scala, all of which explicitly support it.

Root cause

// src/resolution/name-matcher.ts:1062-1069
case 'typescript':
case 'javascript':
case 'tsx':
case 'jsx':
  return [
    new RegExp(`\\b${r}\\b\\s*=\\s*new\\s+([A-Za-z_$][\\w.$]*)`), // = new Logger()
    new RegExp(`\\b(?:const|let|var)\\s+${r}\\s*:\\s*([A-Z][\\w.$]*)`), // lg: Logger
  ];

The second pattern requires a leading const|let|var, so it only matches a local variable's own type annotation (const lg: Logger = ...) — never a bare parameter declaration (function use(lg: Logger)), since a parameter has no const/let/var keyword. Compare to Java's equivalent (line ~1078): \b([A-Z][\w.]*)\s+${r}\b\s*[=;,)] — the trailing ) in the character class is exactly what lets it also match a parameter. Kotlin/Swift/Scala go further and drop any keyword requirement at all (\b${r}\b\s*:\s*([A-Z][\w.]*)), matching both val-declarations and typed params uniformly. TS/JS is the only language in this set that excludes the parameter case.

Repro (built v1.2.0, CodeGraph.init + resolveReferences())

// svc.ts
class Logger { log() { return 1; } }
class Other { log() { return 2; } }
export function use(lg: Logger) { return lg.log(); }
export function useOther(o: Other) { return o.log(); }

Callers of both log methods come back empty.

Control: the byte-identical shape in Java —

class Logger { void log() { int a = 1; } }
class Other { void log() { int a = 2; } }
class Use {
  void run(Logger lg) { lg.log(); }
  void runOther(Other o) { o.log(); }
}

— resolves correctly: runLogger.log, runOtherOther.log. Same construct, same ambiguity (two classes sharing the method name, so no single-candidate fallback can paper over it), only the language differs. This isolates the gap to the TS/JS pattern set specifically.

(Note: a single-class, unambiguous version of the TS repro does resolve — through a same-name fallback strategy elsewhere in the resolver, not through type inference. That fallback is exactly what breaks down once there's a second class with the same method name, which is when type inference is supposed to take over and doesn't.)

Impact

TypeScript/JavaScript is the most common language pair in this project's own userbase, so this is a real precision gap wherever a typed parameter is used as a receiver and its method name collides with another class's method of the same name (a common shape for e.g. handler/service interfaces — process(), handle(), execute()).

Suggested fix direction

Add a parameter-shape pattern mirroring Java's, e.g. \b${r}\s*:\s*([A-Z][\w.$]*)\s*[,)] (or simply drop the const|let|var requirement the way Kotlin/Swift/Scala do) — resolveMethodOnType already validates the inferred type actually declares the method, so loosening this is safe against false positives the same way it is for the other languages.

Verification / scope


@colbymchenry — happy to send a PR for the fix (with a regression test pinning the two-ambiguous-classes/typed-param case), or just leave this filed if you'd rather pick it up yourself. Let me know which you'd prefer.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions