You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Unsound: equality with an undefined-typed variable narrows void to never #1041
Unsound: equality with an undefined-typed variable narrows void to never
Minimal reproduction
functiong(): void{}functionf(y: undefined): void{constx=g();if(x===y){constn: never=x;console.log("branch taken at type never:",n);}}f(undefined);
Save this as repro.ts and run:
make check-file-in-docker FILE=repro.ts
Actual behavior
SKJS accepts the program with no errors. It emits only the @typescript-eslint/no-confusing-void-expression warning on the comparison.
At runtime both operands contain undefined, so the branch is taken and the
program prints:
branch taken at type never: undefined
Expected behavior
SKJS must not narrow x to never in a branch that can be taken. tsc --strict --noEmit repro.ts rejects the never assignment because it does
not narrow the void value away.
Cause
evalVarEquality in src/TypeCheckRefine.sk special-cases the literal undefined and jointly filters the internal TVoid and TUndefined units.
When the right operand is an undefined-typed variable, the generic path
instead computes inter(localType, otherType). General intersection treats
those units as distinct, so inter(void, undefined) becomes never even
though their runtime values can be strictly equal.
The warning for observing a void expression does not prevent narrowing. That
is reasonable: observing it is valid JavaScript and TypeScript permits the
comparison.
Relationship to existing void issues
#871 tracks the opposite assignability problem: SKJS rejects undefined where void is expected. The closed design discussion #75 also covers the runtime
meaning of void. Neither reports this equality-narrowing path or its
reachable never result. No equivalent test, issue, or PR was found.
Suggested fix
For equality narrowing, account for the fact that void and undefined can
contain the same runtime value. When either operand has one of those unit
types, keep or remove both units together, as the existing literal-undefined
path already does. This closes the bug independently of whether #871 changes
general assignability or uses a narrower special case.
Tested against main at bafdcf7c62f921b3064a358d95907a0ea5703037.
Fixed by #1059 (stacked on #1058): void & undefined is now undefined, not never — STypes::interWith crosses a void unit with an undefined unit on the other side (their tags differ, so the tag-keyed pairing never met them), and SType::overlaps reports them as overlapping. In if (x === y) with x: void, y: undefined, the true branch now narrows x to undefined, so const n: never = x is rejected. Pinned in tests/expect-error/void.ts.
Activity
fix: void and undefined overlap in equality narrowing (#1041)
mbouaziz commentedon Nov 4, 2026
Fixed by #1059 (stacked on #1058):
void & undefinedis nowundefined, notnever—STypes::interWithcrosses a void unit with an undefined unit on the other side (their tags differ, so the tag-keyed pairing never met them), andSType::overlapsreports them as overlapping. Inif (x === y)withx: void,y: undefined, the true branch now narrowsxtoundefined, soconst n: never = xis rejected. Pinned intests/expect-error/void.ts.— Mistral Vibe
fix: void and undefined overlap in equality narrowing (#1041)
fix: void and undefined overlap in equality narrowing (#1041)
fix: void and undefined overlap in equality narrowing (#1041)
fix: void and undefined overlap in equality narrowing (#1041)