Sarah DayanSarah Dayan← Blog

Dinero.js v2 is out!

9 min read

After several years of development and 17 alpha releases, Dinero.js v2 stable is here.

It's a full rewrite of the library, with a different architecture, different API, different trade-offs. This post covers what changed, why, and what you can do with it.

What is Dinero.js

Dinero.js is a JavaScript library for creating, calculating, and formatting money. Money is represented as an integer in minor units (cents, pence, etc.) along with a currency. This avoids the floating-point precision issues that plague naive money handling.

0.1 + 0.2; // 0.30000000000000004

const d1 = dinero({ amount: 10, currency: USD });
const d2 = dinero({ amount: 20, currency: USD });

add(d1, d2); // a Dinero object representing $0.30

What's new

Functions instead of methods

In v1, Dinero objects exposed a chainable method API:

import Dinero from 'dinero.js';

Dinero({ amount: 500, currency: 'USD' })
  .add(Dinero({ amount: 100, currency: 'USD' }))
  .toFormat('$0,0.00');

Every Dinero object carried every available operation on its prototype. Bundlers had no way to know which methods you used, so they all ended up in your production bundle.

Dinero.js v2 uses standalone, tree-shakeable functions:

import { dinero, add, toDecimal } from 'dinero.js';
import { USD } from 'dinero.js/currencies';

const price = dinero({ amount: 500, currency: USD });
const fee = dinero({ amount: 100, currency: USD });

toDecimal(add(price, fee)); // "6.00"

You import what you need. Everything else gets eliminated at build time.

This also means functions compose well with functional utilities like Ramda. If you prefer chaining, feel free to build your own wrapper.

Currencies as data

In v1, a currency was a string:

Dinero({ amount: 500, currency: 'USD' });

The library had no way to know what 'USD' meant, how many decimal places it has, or what its subdivision structure is. You had to track that yourself, or rely on the default precision of 2.

In v2, a currency is a structured object with a code, a base, and an exponent:

import { USD } from 'dinero.js/currencies';

// USD = { code: 'USD', base: 10, exponent: 2 }

dinero({ amount: 500, currency: USD });

Dinero.js ships 166 ISO 4217 currencies with correct metadata. A Dinero object's scale (how many decimal places it represents) defaults to the currency's exponent. This means 500 with USD means $5.00 without any extra configuration.

This also unlocks non-decimal currencies. The Mauritanian ouguiya subdivides into 5 khoums. Pre-decimal British currency used 12 pence per shilling and 20 shillings per pound. You can represent these by setting base to a non-decimal value, or to an array for multi-subdivision currencies:

const GBP_OLD = { code: 'GBP', base: [20, 12], exponent: 1 };

Compile-time currency safety

When you use TypeScript and the built-in currencies, Dinero.js catches currency mismatches at compile time:

import { dinero, add } from 'dinero.js';
import { USD, EUR } from 'dinero.js/currencies';

const d1 = dinero({ amount: 500, currency: USD }); // Dinero<number, 'USD'>
const d2 = dinero({ amount: 100, currency: EUR }); // Dinero<number, 'EUR'>

add(d1, d2); // Type error: 'EUR' is not assignable to 'USD'

Each Dinero object carries its currency code as a literal type parameter. Binary operations like add, subtract, or compare enforce that both operands share the same currency type. The type flows through single-operand operations, too. Functions like multiply, allocate, and trimScale all preserve the currency type of their input.

The convert function updates the type to match the target currency:

const d1 = dinero({ amount: 500, currency: USD });
const d2 = dinero({ amount: 100, currency: EUR });

const converted = convert(d1, EUR, rates); // Dinero<number, 'EUR'>

add(converted, d2); // OK
add(converted, d1); // Type error

If you define custom currencies, you can opt into this with as const satisfies:

import type { DineroCurrency } from 'dinero.js';

const BTC = {
  code: 'BTC',
  base: 10,
  exponent: 8,
} as const satisfies DineroCurrency<number, 'BTC'>;

The type parameter defaults to string, so there's no enforcement unless you opt in.

Pluggable calculators

Dinero.js delegates arithmetic to a calculator object. The default calculator implements the number type, which handles most use cases. Still, number has bounds (from -(2^53 - 1) to 2^53 - 1) beyond which it stops producing accurate values.

When you need to go beyond that (large financial amounts, cryptocurrency), you can switch to native bigint. Dinero.js provides a built-in bigint-compatible API:

import { dinero, add } from 'dinero.js/bigint';
import { USD } from 'dinero.js/bigint/currencies';

const d = dinero({ amount: 500n, currency: USD });

Import from dinero.js/bigint instead of dinero.js, and the entire library operates on bigint values. The API is identical: same functions, same behavior, different underlying type.

If you need something else (like big.js for environments without bigint support), you can implement your own calculator and create a custom dinero function with createDinero:

import { createDinero, DineroCalculator } from 'dinero.js';
import Big from 'big.js';

const calculator: DineroCalculator<Big> = {
  add: (a, b) => a.plus(b),
  subtract: (a, b) => a.minus(b),
  multiply: (a, b) => a.times(b),
  // ...
};

const bigDinero = createDinero({ calculator });

All Dinero.js functions are generic, so you can use them with whatever numeric representation fits your constraints.

Scaled amounts

Dinero.js v1 accepted doubles in functions like multiply or percentage. It worked for common cases, but relied on internal float-correction heuristics.

Dinero.js v2 takes a different approach. When you need to express a non-integer multiplier, you pass an integer paired with a scale. These are scaled amounts.

// Multiply by 5.5% tax rate

multiply(price, { amount: 55, scale: 3 }); // 55 / 10^3 = 0.055

No doubles cross the boundary. The Dinero object's scale adjusts to preserve precision. You can bring it back down with trimScale when you're done calculating:

trimScale(multiply(price, { amount: 55, scale: 3 }));

Allocate, don't divide

Dinero.js v1 had divide and percentage methods. Dinero.js v2 removes both.

Division on integers loses information. $10 divided by 3 is $3.33, but 3 times $3.33 is $9.99, not $10. A cent vanished. This is exactly why Martin Fowler's pattern does not have either concepts.

The allocate function, already present in v1, solves both use cases by distributing the amount across ratios, assigning any remainder to the largest share:

const d = dinero({ amount: 1000, currency: USD });

allocate(d, [1, 1, 1]); // [$3.34, $3.33, $3.33]

Every cent is accounted for. This is how you split bills, compute taxes, apply discounts, or distribute payments across installments.

Formatting

Dinero.js v1 had a built-in toFormat method with a mask syntax ('$0,0.00') and an embedded locale system (Dinero.globalLocale, setLocale).

Dinero.js v2 separates concerns. The toDecimal function returns a plain decimal string:

const d = dinero({ amount: 1050, currency: USD });

toDecimal(d); // "10.50"

To format with currency symbols and locale-specific separators, you can compose it with the native Intl.NumberFormat function:

const formatter = ({ value, currency }) => {
  return new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency: currency.code,
  }).format(value);
};

toDecimal(d, formatter);

This keeps the library small and gives you full control over formatting. toDecimal accepts a transformer function that receives the decimal value and currency metadata, so you can return whatever format you need.

For non-decimal currencies, toUnits returns an array of unit values at each subdivision level.

No global state

Dinero.js v1 had mutable global configuration: Dinero.defaultCurrency, Dinero.globalLocale, Dinero.globalFormat, Dinero.globalRoundingMode. Changing one affected every Dinero object created afterwards, anywhere in your application.

Dinero.js v2 has no global state. Every operation is explicit. If you need shared defaults, feel free to write your own custom factory functions.

Native TypeScript

Dinero.js v1 came with community-maintained type definitions via DefinitelyTyped. Dinero.js v2 is written in TypeScript. Types are accurate, always in sync, and shipped with the package.

Upgrading from v1

The upgrade guide covers every breaking change with before/after examples. It also covers alpha upgrades from the deprecated @dinero.js/* packages.

Demos

The documentation includes six interactive demos showing Dinero.js in real-world scenarios. If you learn better by reading code, you can check them out on the repository.

They work with Claude Code, Cursor, GitHub Copilot, and 40+ other agents.

Getting started

Ready to safely manage money in your application? Install the library with your favorite package manager, and get started.

npm install dinero.js
import { dinero, add, toDecimal } from 'dinero.js';
import { USD } from 'dinero.js/currencies';

const price = dinero({ amount: 1999, currency: USD });
const tax = dinero({ amount: 160, currency: USD });

toDecimal(add(price, tax)); // "21.59"

If you use AI coding agents, Dinero.js publishes agent skills that teach your agent correct usage patterns, common pitfalls, and best practices. Install them with:

npx skills add dinerojs/skills

Dinero.js v1 came out in 2018, and the v2 rewrite started in 2021. Between then and now, life moved in ways that made open source sit on the back burner. Shipping this release means a lot to me, because Dinero.js is my first-ever open-source library, and because I finished something I started a long time ago.

Thank you to everyone who filed issues, opened pull requests, used the alphas in production, and kept asking when stable was coming. You kept this project alive when I couldn't.

If you run into issues, find something missing, or have ideas for what should come next, open an issue on GitHub. I'm listening.