A Dart menu importer passes native tests, but compiled to JavaScript for the web it prints 20.0 as 20, sees 1.0 as an int and garbles large item IDs. Why?
answer
- one number representation in JavaScript
- int is a double without fraction
- 2^53 precision ceiling
- toString and is int differ
- bitwise ops truncate to 32 bits
basics
~20 sWhen Dart compiles to JavaScript, int and double share one 64-bit floating-point representation, so an int is a double with no fractional part. 1.0 is int is true, 20.0 prints as 20, and integers beyond 2^53 lose precision.
solid answer
~50 sOn native platforms `int` is a 64-bit two's-complement integer and `double` a separate 64-bit float, so no value is both. When Dart compiles to JavaScript there is only one number representation, the 64-bit double, and both types map to it: an `int` is a double with a zero fractional part. dart.dev lists the consequences: `1.0 is int` and `1 is double` are both true, `1.0.runtimeType` is `int`, `(10.0 * 2).toString()` gives `20` rather than `20.0`, `identical(1.0, 1)` is true, integers above 2^53 lose precision so `2^53 + 1` equals `2^53`, and bitwise and shift operators truncate operands to 32-bit unsigned values. Equality with `==` behaves the same on both. Fixes: format prices explicitly (for example integer cents plus `toStringAsFixed(2)`), keep 64-bit IDs as `String` or use `BigInt`, never branch on `is int` for JSON numbers, and write tests that do not depend on number formatting.
code
dart · 15 linesimport 'dart:math' as math;
void main() {
final total = 10.0 * 2;
print('$total'); // native: 20.0 JavaScript: 20
print(1.0 is int); // native: false JavaScript: true
print(math.pow(2, 53) + 1); // native: 9007199254740993 JavaScript: 9007199254740992
print(-1 >> 0); // native: -1 JavaScript: 4294967295
// Platform-resilient choices:
const priceCents = 1999;
print((priceCents / 100).toStringAsFixed(2)); // 19.99 on both
const itemId = '9223372036854775807'; // keep 64-bit IDs as strings
print(itemId);
}go deeper
Know that on the web Dart numbers are JavaScript doubles, so ints above 2^53 are not exact.
Explain why 1.0 is int is true on the web, how toString differs, and why == still agrees across platforms.
Diagnose web-only numeric bugs from their symptoms and fix them with integer cents, string IDs, explicit formatting and resilient tests.
Decide how shared Dart code guards numeric behaviour across native and web targets, including which data types cross the wire.
## Two platforms, two number models Dart has two user-visible number types, `int` and `double`, both subtypes of `num`. How they are implemented depends on the compilation target: | | Native `int` | Native `double` | JavaScript `int` | JavaScript `double` | |---|---|---|---|---| | Representation | 64-bit two's complement | 64-bit IEEE 754 | 64-bit IEEE 754 | 64-bit IEEE 754 | | Range or precision | -2^63 to 2^63-1 | about 15-17 significant digits | exact only up to 2^53 | same as native | dart.dev's *Numbers in Dart* page explains that on the web, where Dart compiles to JavaScript, Dart maps both types onto JavaScript's single double representation for efficiency. The visible hierarchy is unchanged, but the hidden implementation of `int` also implements `double`. ## What the importer saw 1. **Formatting.** On the web, Dart defers to JavaScript for number-to-string conversion. `1.0.toString()` is `"1"` and `(0.5 + 0.5).toString()` is `"1"`, while native prints `"1.0"`. A price computed as `10.0 * 2` interpolates as `20` instead of `20.0`. 2. **Type tests.** `x is int` is true for any number with a zero fractional part. `1.0 is int`, `1 is double` and even `double.infinity is int` are true on the web; `1.0.runtimeType` is `int`. Code that branches on `is int` to decide whether a JSON value is a whole number or a price behaves differently. 3. **Precision.** Integers are exact only up to 2^53. `math.pow(2, 53) + 1` prints `9007199254740993` natively but `9007199254740992` on the web. A 64-bit item ID from a backend can silently change. 4. **Overflow.** Natively, `2^63` wraps to `-2^63`; on the web it does not overflow, it becomes an approximation. 5. **Bitwise operations.** `&`, `|`, `^`, `~`, `<<`, `>>` and `>>>` use JavaScript's operators, which truncate to 32-bit unsigned values. `-1 >> 0` is `-1` natively and `4294967295` on the web. 6. **Identity.** `identical(1.0, 1)` is true on the web and false natively, although `1.0 == 1` is true on both. dart.dev labels these behaviours **platform-specific and subject to change**, which is itself a reason not to depend on them. ## Fixing the importer - **Prices**: store them as integer cents (`int`), which are exact on both platforms far below 2^53, and format explicitly, for example `(cents / 100).toStringAsFixed(2)`, instead of relying on `toString()`. - **IDs**: keep 64-bit identifiers as `String`. If arithmetic is required, `BigInt` gives arbitrary precision on both platforms, and the `fixnum` package provides strict 64-bit integers; dart.dev warns both cost code size and speed. - **Type decisions**: never use `is int` versus `is double` to interpret data. Decide from the schema, and convert with `toInt()` or `toDouble()` where needed. - **Bits**: operate on 32-bit chunks and use `toSigned(32)` when a signed view is needed. - **Tests**: compare numbers, not strings, or build expected strings the same way, as in dart.dev's `"${20.0} cows"` example. ## Checklist for code shared by native and web - Search for `is int` and `is double` on values that came from outside, and replace them with schema-driven decisions. - Search for `toString()` or plain interpolation of doubles in user-facing text, and switch to explicit formatting. - Check every identifier, timestamp in microseconds or counter that can exceed 2^53, and keep it as a `String` or `BigInt`. - Check bit manipulation on values that may be negative or wider than 32 bits. - Run the unit tests on both targets in CI, since native-only runs hide every one of these differences. Most apps pass all five checks without changes, which is why dart.dev says number differences are rarely a problem; the checklist finds the few places where they are. ## Why it is a senior question Nothing here fails in the analyzer or in native unit tests, and most everyday arithmetic, like counting or indexing, behaves identically. The bug appears only in the web build, in data at the edges. Recognizing the symptoms, a missing `.0`, a type test that is true too often, a trailing-digit change in an ID, and tracing them to the number model is the diagnostic skill being tested.
- Does == between numbers behave differently on the Dart web platform?No. dart.dev's tables show equality results are the same natively and on the web, for example `1.0 == 1` is true on both and `double.nan == double.nan` is false on both. What differs is `identical`, type tests, formatting and precision.
- When would you reach for BigInt or the fixnum package in a Dart web app?When integer values exceed 2^53 and you must do arithmetic on them, such as 64-bit counters or checksums. `BigInt` is arbitrary precision everywhere and `fixnum` gives strict 64-bit behaviour on the web, but dart.dev warns both make code bigger and slower, so plain `String` is better for IDs you only store and compare.
saying these in an interview costs you the question
- Dart int is always a 64-bit integer, on every platform
- 1.0 is int is false everywhere, so is int safely detects whole numbers
- Number formatting via toString() is identical on native and web
- The web platform changes the result of == between int and double
- Bitwise operators behave the same on negative numbers everywhere