21 · Money & safety

Can I trust it with money, and with input I don't control?

Yes. Money is exact, an invariant like non-negative is a type the boundary enforces, and untrusted input cannot reach a SQL or shell sink without being sanitized. Each of these is a compile-or-parse error if you get it wrong, not a convention someone has to remember three functions away.

Money is not a float

A financial system on IEEE-754 number is a bug waiting to be reported: 0.1 + 0.2 is not 0.3, and value past 253 silently loses precision. std/decimal is exact base-10 fixed-point over BigInt. Add, subtract, and multiply are exact; division takes an explicit scale and rounds half away from zero; a malformed amount parses to an Err, never a silent NaN.

std/decimal
let price = decimal("10.50")?
let tax   = decimal("0.84")?
price.add(tax).to_string()      # "11.34", exact

decimal("2")?.div(decimal("3")?, 2)   # "0.67", half away from zero
the bug it removes
// JS number, the default everywhere:
0.1 + 0.2            // 0.30000000000000004
1_000_000_000_000.10 * 1.07   // rounding drift
9007199254740993     // === 9007199254740992

// None of these are your money anymore.

Ids and counters stay exact past 253

An account id or a ledger counter that overflows the float range and rounds is a different kind of catastrophe. bigint is an exact arbitrary-precision integer with 123n literals, and a record field typed bigint validates typeof === "bigint" at the boundary, so an id arriving as a JSON number is rejected rather than quietly truncated.

An invariant is a type, not a comment

"The amount is non-negative" and "the rating is 1 to 5" are the checks that get written once and forgotten later. A where refinement puts the invariant in the type, and the descriptor enforces it at the boundary. A value that fails the predicate never becomes an Amount in the first place.

refinements.glyph
type Amount = int where value >= 0
type Rating = int where value >= 1 && value <= 5

Amount.parse(-1)   # Err  (not just "is a number")
Rating.parse(6)    # Err
Amount.parse(3.5)  # Err  (int check too)

Injection is a compile error

The classic remote-code path is untrusted input reaching a SQL query, a shell command, or an HTML template. std/taint makes that a type error. A value from outside is Tainted; a sink you want to protect takes Trusted. The two are structurally distinct, so the only way from one to the other is an explicit sanitize. Hand a sink the tainted value directly and tsc refuses to compile it.

the safe path compiles
fn run_query(sql: Trusted<string>) -> void { ... }

let input: Tainted<string> = taint(req.body)
run_query(sanitize(input, escape))   # ok
the injection path does not
let input: Tainted<string> = taint(req.body)
run_query(input)

// TS2345: 'Tainted<string>' is not
// assignable to 'Trusted<string>'.

Where it stands

Shipping today

std/decimal for exact money, bigint for exact large integers, where refinements so an invariant is validated at the boundary, and std/taint so untrusted input can't reach a protected sink without an explicit sanitize. Each is checked: a bad amount or id fails parse, a bad invariant fails parse, and an unsanitized value fails tsc.