BigDecimal
BigDecimal represents a decimal number with arbitrary precision. It exists to
avoid the rounding errors of JavaScript’s binary floating-point number, where
0.1 + 0.2 === 0.30000000000000004. When you are working with money, financial
calculations, or any domain where exactness matters, use BigDecimal instead of
number. A BigDecimal stores an arbitrary-precision integer (value, a
bigint) together with a scale (the number of digits after the decimal
point).
import { BigDecimal } from "effect"
// Floating point: wrongconsole.log(0.1 + 0.2) // 0.30000000000000004
// BigDecimal: exactconst sum = BigDecimal.sum( BigDecimal.fromStringUnsafe("0.1"), BigDecimal.fromStringUnsafe("0.2"))console.log(BigDecimal.format(sum)) // "0.3"fromStringUnsafe parses a decimal literal exactly, and BigDecimal.sum adds
two values with no loss of precision. BigDecimal.format renders the result
back to a string.
Creating a BigDecimal
Section titled “Creating a BigDecimal”The safest constructors return an Option, because not every input is a valid
decimal:
import { BigDecimal } from "effect"
// From a string — None if the string is not a valid numberconst fromStr = BigDecimal.fromString("123.45") // Some(123.45)const invalid = BigDecimal.fromString("nope") // None
// From a number — None if not finite (NaN / Infinity)const fromNum = BigDecimal.fromNumber(123.45)When you control the input (e.g. a hard-coded literal) the *Unsafe variants
return a BigDecimal directly and throw on bad input:
import { BigDecimal } from "effect"
const price = BigDecimal.fromStringUnsafe("19.99")Note the v4 convention: the throwing variant carries the Unsafe suffix
(fromStringUnsafe), while the total version returns an Option
(fromString).
Arithmetic
Section titled “Arithmetic”sum, subtract, and multiply are exact and return a BigDecimal. Division
returns an Option, because dividing by zero has no result:
import { BigDecimal, Option } from "effect"
const a = BigDecimal.fromStringUnsafe("10.5")const b = BigDecimal.fromStringUnsafe("3")
console.log(BigDecimal.format(BigDecimal.sum(a, b))) // "13.5"console.log(BigDecimal.format(BigDecimal.subtract(a, b))) // "7.5"console.log(BigDecimal.format(BigDecimal.multiply(a, b))) // "31.5"
// divide is partial: None when dividing by zeroconst quotient = BigDecimal.divide(a, b)console.log(Option.map(quotient, BigDecimal.format))// { _id: 'Option', _tag: 'Some', value: '3.5' }
console.log(BigDecimal.divide(a, BigDecimal.fromStringUnsafe("0")))// { _id: 'Option', _tag: 'None' }Rounding
Section titled “Rounding”BigDecimal.round rounds to a given number of decimal places using a rounding
mode:
import { BigDecimal } from "effect"
const value = BigDecimal.fromStringUnsafe("3.14159")
// Round to 2 decimal placesconsole.log(BigDecimal.format(BigDecimal.round(value, { scale: 2 })))// "3.14"Comparison
Section titled “Comparison”Use BigDecimal.equals for value equality. Because comparison is numeric,
values with different scales but the same magnitude are equal:
import { BigDecimal } from "effect"
console.log( BigDecimal.equals( BigDecimal.fromStringUnsafe("1.0"), BigDecimal.fromStringUnsafe("1.00") )) // trueA worked example: money
Section titled “A worked example: money”Modelling currency is the canonical use case. Keeping amounts as BigDecimal
guarantees totals add up to the cent.
import { BigDecimal } from "effect"
const lineItems = ["19.99", "5.49", "12.00"].map(BigDecimal.fromStringUnsafe)
// sumAll folds a collection without intermediate roundingconst subtotal = BigDecimal.sumAll(lineItems)
const taxRate = BigDecimal.fromStringUnsafe("0.08")// Round the tax to 2 decimal places (cents)const tax = BigDecimal.round(BigDecimal.multiply(subtotal, taxRate), { scale: 2 })const total = BigDecimal.sum(subtotal, tax)
console.log(BigDecimal.format(subtotal)) // "37.48"console.log(BigDecimal.format(total)) // subtotal plus rounded tax, exact to the centFor validating and persisting BigDecimal values at the edges of your system,
the Schema module provides matching codecs.