Skip to content

tsserver hangs indefinitely when inferring return type of class method that passes this to a function with deeply nested conditional return typeΒ #63207

@kkirby

Description

@kkirby

πŸ”Ž Search Terms

hang circular

πŸ•— Version & Regression Information

This behavior is present in TypeScript 5 and TypeScript 6 beta.

⏯ Playground Link

N/A β€” requires the remeda package (npm i remeda). Reproduction is a self-contained file below.

πŸ’» Code

import { pick } from "remeda";

export class MyClass {
  constructor(public readonly field: number) {}

  // [bad] tsserver hangs indefinitely: no explicit return type
  getIdentity() {
    return pick(this, ["field"]);
  }

  // [good] works fine: explicit return type breaks the cycle
  getIdentityFixed(): Pick<MyClass, "field"> {
    return pick(this, ["field"]);
  }
}

πŸ™ Actual behavior

tsserver becomes completely unresponsive. The editor (VS Code) loses all language service features (hover types, completions, diagnostics) until tsserver is manually restarted. No error is emitted; tsserver simply loops forever.

The cause is an undetected circular type inference chain. To infer the return type of getIdentity(), TypeScript must evaluate PickFromArray<MyClass, ["field"]> (remeda's pick return type). That involves IsBoundedRecord<MyClass> β†’ IsBounded<KeysOfUnion<MyClass>>. KeysOfUnion uses UnionToIntersection, which places MyClass in a contravariant function parameter position β€” forcing eager, non-deferred evaluation of the full structural type of MyClass. The full type of MyClass includes getIdentity: () => <inferred>, whose inferred return type is what we started trying to compute. Because the cycle passes through ~5 distinct type alias instantiations, TypeScript's cycle-detection heuristics never fire, and the checker loops indefinitely instead of emitting a "circularly references itself" error.

πŸ™‚ Expected behavior

TypeScript should detect the circular inference and either:

  • Emit a "Return type annotation circularly references itself" error (as it does for simpler cycles), or
  • Fall back to any with a warning.

Either outcome is acceptable. The checker should never hang.

Additional information about the issue

Workaround: add an explicit return type annotation to the method. This gives TypeScript the type of MyClass.getIdentity() upfront, preventing the cycle from forming.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions