Repository navigation
An optional number compares as the wider type, and only when it holds a value - #423
Merged
ASDAlexander77 merged 1 commit intoSep 30, 2026
Merged
Conversation
… a value `let n: number | undefined = 0.5; n === 0` was true. The numeric sync in adjustTypesForBinaryOp looked for the first promotion type either operand had exactly; an optional never is one, so the other operand's type won - an integer literal's si32 - and the optional was cast out to it, 0.5 becoming 0. The same cast read an empty optional as 0: `undefined === 0`, `undefined < 1` were true. A comparison with one optional numeric side now widens both to the wider of the optional's element type and the other operand's type, and keeps the optional, so the lowering compares only when it holds a value (OptionalLogicHelper). `-` and `*`, which share the case, are left as they were. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ASDAlexander77
added a commit
that referenced
this pull request
Sep 30, 2026
…value (#422) * `T | null | undefined` accepts null, and an empty optional equals no value - Strict null checks: `string | null | undefined` (and a class) became optional<string> without the null the checker needs, so null was a compile error. The null member now goes in before undefined wraps the type: optional<union<string, null>>. - Without strict null checks a nullable type still absorbs null, but a union of several types is a tagged value, not one pointer: `string | number | null` keeps null as a member (it was a verify error to store null in it). - `number | null | undefined` holding null crashed the lowering on `=== null`: LogicOp read the union tag with ExtractValueOp from a ts.union value. An optional's unwrapped value arrives unconverted. - An optional compared with a value was unwrapped without asking whether it holds one, so an empty `string | undefined` was `=== null` and an empty `boolean | undefined` `=== false`. The values are compared only when it holds one; loosely it still equals null. - A value cast to `T | null` is cast to T first: an object literal to `Interface | null`, a const array to `number[] | null`, were left for the lowering, which cannot convert them. - The `// @strict-null` pragma never reached the type helper's copy of the options, so unions were built strict in files compiled non-strict. The lowering-error test used `number[] | null = [1, 2]` to raise its error, which now compiles; it reads the array out of a `number[] | null` instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * An optional number compares as the wider type, and only when it holds a value (#423) `let n: number | undefined = 0.5; n === 0` was true. The numeric sync in adjustTypesForBinaryOp looked for the first promotion type either operand had exactly; an optional never is one, so the other operand's type won - an integer literal's si32 - and the optional was cast out to it, 0.5 becoming 0. The same cast read an empty optional as 0: `undefined === 0`, `undefined < 1` were true. A comparison with one optional numeric side now widens both to the wider of the optional's element type and the other operand's type, and keeps the optional, so the lowering compares only when it holds a value (OptionalLogicHelper). `-` and `*`, which share the case, are left as they were. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Stacked on #422 (base
fix-null-undefined-unions). It relies on that PR's check inOptionalLogicHelperthat the optional holds a value. Merge #422 first, then retarget this PR tomain.Cause
adjustTypesForBinaryOpsyncs numeric operands by walking the promotion order (number,f64, …,si32, …). It picks the first type that one of the operands has exactly. An optional is never exactly one of them, so the other operand's type won:si32. The optional was cast out to it, and 0.5 became 0.numbervariable: the type wasnumber, which is right. But casting out of the optional read an empty one as 0 (ts.If/ts.Default), soundefined === zerowas true.Change
syncOptionalNumericTypesruns for comparisons with exactly one optional numeric side:T | null | undefinedaccepts null, and an empty optional equals no value #422).-and*share thatcase, and are left as they were.Tests
00optional_number_compare.ts(compile and JIT). It covers a fraction against integer literals andnumbervariables, an empty optional against both,s32 | undefined, and the literal on either side. It was seen failing first (assertion failed: 0.5 === 0).ctest -C Release3323/3323.🤖 Generated with Claude Code