Why Coins?
Monetary arithmetic with number accumulates IEEE-754 rounding errors. These errors are invisible in tests but show up in production totals, allocation remainders, and exchange results.
ts
// Before — float arithmetic
const price = 10.1 + 10.2; // 20.299999999999997, not 20.3
const [a, b, c] = [price / 3, price / 3, price / 3];
a + b + c; // 20.299999999999997 — penny lost
// After — bigint minor units
import { add, allocate, money, toDecimal } from '@vielzeug/coins';
const price = add(money('10.10', 'USD'), money('10.20', 'USD'));
const [a, b, c] = allocate(price, [1, 1, 1]);
a.amount + b.amount + c.amount === price.amount; // true — always| Feature | Coins | Dinero.js v2 | currency.js |
|---|---|---|---|
| Bundle size | 3.5 KB | ~14 kB | ~2.5 kB |
| Zero dependencies | |||
bigint minor units | |||
| TypeScript-native | |||
| Validated currency codes | |||
| Locale-aware formatting | |||
| Largest Remainder allocation |
Use Coins when you need exact bigint arithmetic with validated currencies, typed allocation, and Intl-powered formatting in a single zero-dependency package.
Consider Dinero.js when your team already uses it and float precision is acceptable for your use case.
Installation
sh
pnpm add @vielzeug/coinssh
npm install @vielzeug/coinssh
yarn add @vielzeug/coinsQuick Start
ts
import { add, allocate, exchange, format, money, multiply } from '@vielzeug/coins';
import type { ExchangeRate, Money } from '@vielzeug/coins';
// Create money from decimal strings (lossless) or bigint minor units
const price: Money = money('19.99', 'USD'); // { amount: 1999n, currency: 'USD' }
const tax: Money = money('1.60', 'USD');
const total: Money = add(price, tax); // { amount: 3559n, currency: 'USD' }
// Arithmetic
multiply(total, '1.1'); // $39.15 (half-away-from-zero, default)
multiply(total, '1.1', 'floor'); // explicit rounding mode
// Lossless allocation — no minor unit is ever lost or gained
allocate(money('10.00', 'USD'), [1, 1, 1]); // [$3.34, $3.33, $3.33]
// Locale-aware formatting
format(total); // '$35.59'
format(total, { locale: 'de-DE' }); // '35,59 $'
format(total, { style: 'code' }); // 'USD 35.59'
// Currency exchange — ExchangeRate.from/to are plain strings; rate is a decimal string
const rate: ExchangeRate = { from: 'USD', rate: '0.92', to: 'EUR' };
exchange(total, rate); // { amount: 3274n, currency: 'EUR' }Features
money()— create from decimal string, number, or bigint minor units; currency validated at creation time viaIntl; dev warning when float has more decimals than currency supports- Arithmetic —
add,subtract,multiply,divide,abs,negate; all throwCurrencyMismatchErroron currency mismatch roundTo()— round to fewer decimal places (e.g. whole dollars); configurable rounding mode- Allocation —
allocate(weighted) andsplitEvenly(equal); Largest Remainder Method guarantees exact totals - Aggregates —
sum,min,max,clamp;min/maxaccept a non-empty array - Comparison —
compare,isEqual(returnsfalseon currency mismatch),greaterThan,greaterThanOrEqual,lessThan,lessThanOrEqual,isZero,isPositive,isNegative,isNonNegative,isNonPositive format()—Intl.NumberFormat-powered string output with symbol / code / name / narrowSymbol stylesformatParts()— typed part array for custom UI rendering (superscript cents, coloured symbols, etc.)exchange()— currency conversion using string rates;ExchangeRate.from/toare plain strings; throwsCurrencyMismatchErroron mismatch- Serialization —
toDecimal,toNumber,toJSON,fromJSON; safebigintround-trip through JSON withAmount()— clone aMoneywith a new bigint amount, preserving the currencyisMoney()— type guard for narrowing unknown payloads; own-property check guards against prototype pollutionCurrencyMismatchError/InvalidCurrencyError— typed error subclasses for structuredcatchblocks