Money CorrectnessAug 23, 2024 · 2 min read

A Money Extension Type That Makes 0.1 + 0.2 == 0.3 Fail Loudly, Not Silently

Dart's extension types turn a money precision bug from a runtime surprise into a compile error, at zero runtime cost. Here's the pattern.


0.1 + 0.2 == 0.3 evaluates to false in Dart, for the same reason it does in almost every language: binary floating point can't represent most decimal fractions exactly (0.1 + 0.2 actually comes out to 0.30000000000000004). It's a well-known trap, and "just don't use double for money" is well-known advice. The gap is usually enforcement — nothing stops a teammate, six months from now, from doing exactly that in a hurry.

The pattern: wrap it so the compiler enforces it #

Dart 3's extension types are zero-cost wrappers: no runtime object, no boxing, just a compile-time view over an underlying representation that restricts what you can do with it.

money.dart
extension type Money._(int minorUnits) {
  factory Money.fromMinorUnits(int minorUnits) = Money._;
 
  Money operator +(Money other) => Money._(minorUnits + other.minorUnits);
  Money operator -(Money other) => Money._(minorUnits - other.minorUnits);
 
  bool operator >(Money other) => minorUnits > other.minorUnits;
 
  double get asMajorUnits => minorUnits / 100;
 
  @override
  String toString() => (minorUnits / 100).toStringAsFixed(2);
}

Store everything in integer minor units (cents, paise — whatever your smallest denomination is) under the hood. The extension type means Money only supports the operations you defined on it.

What this actually buys you #

final balance = Money.fromMinorUnits(1000); // ₹10.00
final fee = 0.2; // someone reached for a raw double
 
balance + fee; // compile error: no operator matching (Money, double)

That's the entire point. A precision bug that would previously surface as a support ticket — "my balance is off by a fraction of a cent" — instead fails the build, in the IDE, before it's ever committed. Money has no + overload that accepts a double, so there's no code path where one silently gets summed against minor units.

Worth noting

This isn't a runtime validation library — there's no exception to catch, because there's nothing to catch. The type system rules the mixed arithmetic out before the code compiles, which is a stronger guarantee than any test suite can give you.

Where it still needs a human decision #

Extension types stop you from accidentally mixing money and raw numbers. They don't decide your rounding rule, your display currency, or what happens to a fractional minor unit after a percentage split — those are still product and finance decisions that belong in code review, not in the type system. The type system's job here is narrow and specific: make the easy mistake impossible, and leave the hard decisions visible.

Stuck on this in your own app?

This is the kind of problem I help fintech teams get right the first time — see how App Architecture & Consulting engagements work, or just tell me what you're building.