Skip to content

API Overview

SymbolPurposeExecutionCommon gotcha
moneyConstruct validated moneySyncBigint requires { unit: 'minor' }
currencyResolve built-in or construct customSyncUnknown codes throw
add / subtractCombine matching currenciesSyncMismatches throw
multiply / divideExact decimal scalingSyncUse decimal strings
sumAggregate with inferred currencySyncEmpty iterable requires { currency }
allocateSplit without losing minor unitsSyncWeights must be non-negative
clampBound to min/max rangeSyncMin must not exceed max
exchangeConvert through an exact rateSyncRate source must match value currency
formatPresent money with IntlSyncFormatting does not define currency scale
toJSON / decodeMoneyCross serialization or realm boundarySyncPersisted amount uses minor units

Package Entry Point

ImportPurpose
@vielzeug/coinsComplete public Coins API

Construction

currency(code) / currency({ code, minorUnit })

ts
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.

ts
currency('USD');
currency({ code: 'PTS', minorUnit: 0 });

isCurrency(value)

ts
function isCurrency(value: unknown): value is Currency;

Type guard for canonical currency values (built-in or constructed via currency()).

money(amount, currency, options?)

ts
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>;
ts
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?)

ts
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)

ts
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

ts
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

ts
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

ts
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>;
ts
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)

ts
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

ts
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

ts
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

ts
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 modeBehavior
awayFromZeroAlways increase a non-exact magnitude
ceilToward positive infinity
floorToward negative infinity
halfAwayFromZeroNearest; midpoint away from zero
halfEvenNearest; midpoint to an even retained unit
towardZeroTruncate toward zero

Errors

ts
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.

ts
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.