-
Notifications
You must be signed in to change notification settings - Fork 13.3k
Description
π 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.