API Overview
| Symbol | Purpose | Execution | Common gotcha |
|---|---|---|---|
money | Construct validated money | Sync | Bigint requires { unit: 'minor' } |
currency | Resolve built-in or construct custom | Sync | Unknown codes throw |
add / subtract | Combine matching currencies | Sync | Mismatches throw |
multiply / divide | Exact decimal scaling | Sync | Use decimal strings |
sum | Aggregate with inferred currency | Sync | Empty iterable requires { currency } |
allocate | Split without losing minor units | Sync | Weights must be non-negative |
clamp | Bound to min/max range | Sync | Min must not exceed max |
exchange | Convert through an exact rate | Sync | Rate source must match value currency |
format | Present money with Intl | Sync | Formatting does not define currency scale |
toJSON / decodeMoney | Cross serialization or realm boundary | Sync | Persisted amount uses minor units |
Package Entry Point
| Import | Purpose |
|---|---|
@vielzeug/coins | Complete public Coins API |
Construction
currency(code) / currency({ code, minorUnit })
function currency<C extends string>(code: C): Currency<C>;
function currency<C extends string>(definition: { code: C; minorUnit: number }): Currency<C>;Resolves one of the seven built-ins (USD, EUR, GBP, JPY, KRW, BHD, or KWD) or constructs an immutable custom currency. Custom currencies are local values with no global registry. Code must be exactly three uppercase letters; minorUnit must be an integer from 0 to 6. Arithmetic compares currency definitions by reference.
currency('USD');
currency({ code: 'PTS', minorUnit: 0 });isCurrency(value)
function isCurrency(value: unknown): value is Currency;Type guard for canonical currency values (built-in or constructed via currency()).
money(amount, currency, options?)
function money<C extends Currency>(amount: string, currency: C, options?: { rounding?: RoundingMode }): Money<C>;
function money<C extends Currency>(amount: bigint, currency: C, options: { unit: 'minor' }): Money<C>;money('19.99', USD);
money(1999n, USD, { unit: 'minor' });Decimal strings that exceed the currency's precision require a rounding mode. Inputs use plain base-10 syntax without exponent, leading plus, or whitespace; input length is capped at 1,000 characters and normalized fractional precision at 100 places. Bigint amounts require { unit: 'minor' }.
decodeMoney(value, options?)
function decodeMoney(value: unknown, options?: { currency?: (code: string) => Currency }): Money;Validates untrusted or cross-realm data and returns canonical money. Plain bigint input must have exactly amount and canonical currency; JSON must have exactly amount, currency code, and unit: 'minor'. Serialized amounts are bounded to 1,000 characters. A custom resolver must return a canonical currency whose code matches the envelope.
isMoney(value)
function isMoney(value: unknown): value is Money;Type guard for canonical Coins money values. Checks identity against the internal canonical set: forged frozen objects do not pass.
Arithmetic
function add<C extends Currency>(left: Money<C>, right: Money<NoInfer<C>>): Money<C>;
function subtract<C extends Currency>(left: Money<C>, right: Money<NoInfer<C>>): Money<C>;
function multiply<C extends Currency>(value: Money<C>, factor: string, options?: { rounding?: RoundingMode }): Money<C>;
function divide<C extends Currency>(value: Money<C>, divisor: string, options?: { rounding?: RoundingMode }): Money<C>;
function compare<C extends Currency>(left: Money<C>, right: Money<NoInfer<C>>): -1 | 0 | 1;
function clamp<C extends Currency>(
value: Money<C>,
options: { max: Money<NoInfer<C>>; min: Money<NoInfer<C>> },
): Money<C>;
function abs<C extends Currency>(value: Money<C>): Money<C>;
function negate<C extends Currency>(value: Money<C>): Money<C>;
function round<C extends Currency>(
value: Money<C>,
options: { fractionDigits: number; rounding?: RoundingMode },
): Money<C>;
function toDecimal(value: Money): string;factor and divisor are plain decimal strings; exponent notation, whitespace, and a leading plus are invalid. Matching currency identity is required for binary operations. round() requires integer fractionDigits from 0 through the currency's minorUnit. Scaling and rounding default to halfAwayFromZero.
Aggregation
function sum<C extends Currency>(values: readonly Money<C>[]): Money<C>;
function sum<C extends Currency>(values: Iterable<Money<C>>, options: { currency: C }): Money<C>;
function allocate<C extends Currency>(value: Money<C>, count: number): Money<C>[];
function allocate<C extends Currency>(value: Money<C>, weights: readonly string[]): Money<C>[];sum() infers currency from non-empty values. An empty iterable requires a canonical { currency } and returns canonical zero. allocate() accepts a positive integer count or dense, non-empty, non-negative decimal-string weights with a positive total. Either form is limited to 100,000 output parts. It distributes remainders by largest fractional share, breaks ties by input order, and preserves the exact signed minor-unit total.
Exchange
function exchangeRate<From extends Currency, To extends Currency>({
from,
to,
value,
}: {
from: From;
to: To;
value: string;
}): ExchangeRate<From, To>;
function exchange<From extends Currency, To extends Currency>(
value: Money<From>,
rate: ExchangeRate<From, To>,
options?: { rounding?: RoundingMode },
): Money<To>;const rate = exchangeRate({ from: USD, to: EUR, value: '0.9234' });
exchange(money('100.00', USD), rate);Rates are exact positive decimal strings. The money currency must be the same canonical instance as rate.from. Conversion defaults to halfAwayFromZero rounding.
isExchangeRate(value)
function isExchangeRate(value: unknown): value is ExchangeRate;Type guard for canonical exchange rates created by exchangeRate(). Checks identity against the internal canonical set: forged frozen objects do not pass.
Formatting
function format(value: Money, options?: FormatOptions): string;
function formatParts(value: Money, options?: FormatOptions): MoneyFormatPart[];Defaults are locale en-US, style symbol, and the currency's minor-unit digits. Either fraction bound may be supplied independently; the omitted bound adjusts to remain compatible. Explicit bounds must satisfy 0 ≤ minimum ≤ maximum ≤ 20. Exact decimal strings and mapped rounding modes are passed to Intl.NumberFormat, preserving locale digits, currency-name pluralization, and rounded negative signs. formatParts() keeps locale grouping inside its integer part.
Serialization
function toJSON(value: Money): MoneyJSON;
function decodeMoney(value: unknown, options?: { currency?: (code: string) => Currency }): Money;toJSON() produces exactly { amount, currency, unit: 'minor' }. amount is a canonical integer string of at most 1,000 characters: no decimal point, exponent, leading plus, leading zero, whitespace, or negative zero. decodeMoney() resolves built-ins with currency() by default; custom codes require a resolver returning the same encoded code.
Types
type CurrencyCode<C extends string = string> = C & { readonly [currencyBrand]: C };
type Currency<C extends string = string> = Readonly<{
code: CurrencyCode<C>;
minorUnit: number;
}>;
type Decimal = Readonly<{
readonly [decimalBrand]: true;
denominator: bigint;
numerator: bigint;
}>;
type Money<C extends Currency = Currency> = Readonly<{
amount: bigint;
currency: C;
readonly [moneyBrand]: C;
}>;
type ExchangeRate<From extends Currency = Currency, To extends Currency = Currency> = Readonly<{
from: From;
to: To;
value: Decimal;
}>;
type FormatOptions = Readonly<{
locale?: string;
maximumFractionDigits?: number;
minimumFractionDigits?: number;
rounding?: RoundingMode;
style?: 'code' | 'name' | 'narrowSymbol' | 'symbol';
}>;
type MoneyFormatPart = Readonly<{
type: 'currency' | 'decimal' | 'fraction' | 'integer' | 'literal' | 'minusSign' | 'plusSign';
value: string;
}>;
type MoneyJSON = Readonly<{
amount: string;
currency: string;
unit: 'minor';
}>;
type RoundingMode = 'awayFromZero' | 'ceil' | 'floor' | 'halfAwayFromZero' | 'halfEven' | 'towardZero';CurrencyCode, Decimal, and Money carry phantom brand symbols that prevent unbranded values from being assigned where canonical values are required. ExchangeRate.value exposes a normalized exact Decimal numerator and denominator.
| Rounding mode | Behavior |
|---|---|
awayFromZero | Always increase a non-exact magnitude |
ceil | Toward positive infinity |
floor | Toward negative infinity |
halfAwayFromZero | Nearest; midpoint away from zero |
halfEven | Nearest; midpoint to an even retained unit |
towardZero | Truncate toward zero |
Errors
type CoinsErrorCode =
| 'CURRENCY_MISMATCH'
| 'DIVISION_BY_ZERO'
| 'FORMAT_ERROR'
| 'INVALID_ALLOCATION'
| 'INVALID_CURRENCY'
| 'INVALID_DECIMAL'
| 'INVALID_EXCHANGE_RATE'
| 'INVALID_MONEY'
| 'INVALID_RANGE'
| 'INVALID_ROUNDING';Every Coins failure extends CoinsError and exposes code.
class CoinsError extends Error {
readonly code: CoinsErrorCode;
}
class CurrencyMismatchError extends CoinsError {
readonly expected: string;
readonly received: string;
}
class InvalidCurrencyError extends CoinsError {
readonly value: unknown;
}CurrencyMismatchError and InvalidCurrencyError are specialized CoinsError subclasses. Use instanceof CoinsError to narrow any value to the Coins error hierarchy.