Logo

dev-resources.site

for different kinds of informations.

Building a Type-Safe Money Handling Library in TypeScript

Published at
1/9/2025
Categories
money
currency
react
typescript
Author
thesmilingsloth
Categories
4 categories in total
money
open
currency
open
react
open
typescript
open
Author
15 person written this
thesmilingsloth
open
Building a Type-Safe Money Handling Library in TypeScript

After years of wrestling with money-related bugs in financial applications, from rounding errors in e-commerce platforms to currency conversion mishaps in international payment systems, I decided to tackle these challenges head-on. The result is @thesmilingsloth/money-utils, a TypeScript library that brings bank-grade money handling to JavaScript applications. In this post, I'll share the journey, technical decisions, and lessons learned while building it.

The Money Problem in Software Engineering

Money seems simple at first glance—it's just numbers, right? But as Martin Fowler noted in his patterns of enterprise application architecture, money is one of the most complex domains in software engineering. Here's why:

The Floating-Point Trap

JavaScript's number type uses IEEE 754 floating-point arithmetic, which leads to infamous precision issues:

// The classic example
0.1 + 0.2; // => 0.30000000000000004

// But it gets worse with multiple operations
0.1 + 0.2 + 0.3 + 0.4 + 0.5; // => 1.5000000000000002
(0.1 * 100 + 0.2 * 100) / 100; // => 0.30000000000000004
Enter fullscreen mode Exit fullscreen mode

In financial applications, these tiny discrepancies can accumulate into significant errors. Imagine calculating interest rates or processing thousands of transactions—the errors compound quickly.

Real-World Horror Stories

I've seen these issues manifest in production systems:

  1. An e-commerce platform losing thousands of dollars due to rounding errors in bulk discounts
  2. A payment processor having mismatched totals because of currency conversion rounding
  3. An accounting system showing different balances depending on the calculation path

The Solution: @thesmilingsloth/money-utils

Our library addresses these challenges with several key features:

1. Precise Decimal Arithmetic

We chose decimal.js as our arithmetic engine after careful evaluation. It provides the precision needed for financial calculations while maintaining good performance:

import { Money } from "@thesmilingsloth/money-utils";

// Precise calculations, even with complex operations
const price = new Money("99.99", "USD");
const quantity = 2;

// All operations maintain precision
const subtotal = price.multiply(quantity);
const tax = subtotal.multiply("0.2"); // 20% tax
const total = subtotal.add(tax);

console.log(total.toString()); // "$239.98"
Enter fullscreen mode Exit fullscreen mode

2. Type-Safe Currency Operations

The library treats currencies as first-class citizens, with built-in support for major currencies and custom currency definitions:

import { Money, Currency } from "@thesmilingsloth/money-utils";

// Built-in currencies with sensible defaults
const usd = Currency.USD; // Decimals: 2, Symbol: $
const eur = Currency.EUR; // Decimals: 2, Symbol: €
const jpy = Currency.JPY; // Decimals: 0, Symbol: ¥
const btc = Currency.BTC; // Decimals: 8, Symbol: ₿

// Example of built-in currency configurations
const USDDefaults = {
  name: "US Dollar",
  code: "USD",
  symbol: "$",
  symbolPosition: "prefix",
  decimals: 2,
  minorUnits: "100",
  decimalSeparator: ".",
  thousandsSeparator: ",",
  isCrypto: false,
};

const BTCDefaults = {
  name: "Bitcoin",
  code: "BTC",
  symbol: "",
  symbolPosition: "prefix",
  decimals: 8,
  minorUnits: "100000000",
  decimalSeparator: ".",
  thousandsSeparator: ",",
  isCrypto: true,
};

// Custom currency support
Currency.register({
  name: "Corporate Loyalty Points",
  code: "POINTS",
  symbol: "",
  symbolPosition: "prefix",
  decimals: 0,
  minorUnits: "1",
  decimalSeparator: ".",
  thousandsSeparator: ",",
  isCrypto: false,
});

// Type-safe operations prevent mixing currencies
const dollars = new Money("100.00", "USD");
const euros = new Money("100.00", "EUR");
// This won't compile - TypeScript prevents currency mixing
// const invalid = dollars.add(euros);
Enter fullscreen mode Exit fullscreen mode

3. Configurable Options with Sensible Defaults

One of our key design decisions was providing sensible defaults while allowing customization:

// Default options for Money instances
const defaultMoneyOptions = {
  decimals: undefined, // Uses the currency's default decimals
  displayDecimals: undefined, // Uses the currency's default decimals
  roundingMode: ROUNDING_MODE.ROUND_HALF_UP,
  symbol: undefined, // Uses the currency's default symbol
};

// Using defaults - clean and simple
const simple = new Money("100", "USD");
console.log(simple.toString()); // "$100.00"

// Custom configuration when needed
const custom = new Money("100.555", "USD", {
  decimals: 3, // Store with 3 decimal places
  displayDecimals: 2, // Display with 2 decimal places
  roundingMode: ROUNDING_MODE.ROUND_DOWN,
  symbol: "US$", // Custom symbol
});
console.log(custom.toString()); // "US$100.55"
Enter fullscreen mode Exit fullscreen mode

4. Comprehensive Formatting Options

Our formatting engine goes beyond basic toString() operations:

const amount = new Money("1234567.89", "USD");

// Basic formatting with defaults
console.log(amount.toString()); // "$1,234,567.89"
console.log(amount.formattedValue()); // "1,234,567.89"
console.log(amount.formattedValueWithSymbol()); // "$1,234,567.89"

// Locale-aware formatting
console.log(amount.toLocaleString()); // Uses browser's default locale
console.log(amount.toLocaleString("en-US")); // "$1,234,567.89"
console.log(amount.toLocaleString("de-DE")); // "1.234.567,89 $"
console.log(amount.toLocaleString("ja-JP")); // "¥1,234,567.89"

// Advanced formatting options
console.log(
  amount.toLocaleString("en-US", {
    style: "currency",
    currencyDisplay: "name",
  })
); // "1,234,567.89 US dollars"
Enter fullscreen mode Exit fullscreen mode

Advanced Features and Edge Cases

1. Configurable Rounding Behavior

import { Money, ROUNDING_MODE } from "@thesmilingsloth/money-utils";

const amount = new Money("100.555", "USD", {
  roundingMode: ROUNDING_MODE.ROUND_HALF_UP,
  decimals: 2,
});

// Different rounding modes for different use cases
const roundDown = new Money("100.555", "USD", {
  roundingMode: ROUNDING_MODE.ROUND_DOWN,
});
console.log(roundDown.toString()); // "$100.55"

const roundUp = new Money("100.555", "USD", {
  roundingMode: ROUNDING_MODE.ROUND_UP,
});
console.log(roundUp.toString()); // "$100.56"
Enter fullscreen mode Exit fullscreen mode

2. Cryptocurrency Support

// Bitcoin with 8 decimal places
const bitcoin = new Money("1.23456789", "BTC");
console.log(bitcoin.toString()); // "₿1.23456789"

// Ethereum with 18 decimal places
Currency.register({
  name: "Ethereum",
  code: "ETH",
  symbol: "Ξ",
  symbolPosition: "prefix",
  decimals: 18,
  minorUnits: "1000000000000000000",
  decimalSeparator: ".",
  thousandsSeparator: ",",
  isCrypto: true,
});

const eth = new Money("1.234567890123456789", "ETH");
Enter fullscreen mode Exit fullscreen mode

3. Complex Allocation Scenarios

// Investment profit distribution with different stakeholder percentages
const profit = new Money("1000000.00", "USD");
const shares = profit.allocate([
  15, // 15% - Founder
  30, // 30% - Operations
  20, // 20% - Development
  35, // 35% - Investors
]);

console.log(shares.map((s) => s.toString()));
// [
//   "$150,000.00",
//   "$300,000.00",
//   "$200,000.00",
//   "$350,000.00"
// ]
Enter fullscreen mode Exit fullscreen mode

Technical Decisions and Lessons Learned

  1. Type Safety is Non-Negotiable: TypeScript's type system caught countless potential bugs during development. For example:
   // This won't compile - type safety prevents invalid operations
   const money = new Money("100.00", "USD");
   const result = money.multiply("invalid");
Enter fullscreen mode Exit fullscreen mode
  1. Immutability Prevents Bugs: All operations return new instances, preventing accidental mutations:
   const original = new Money("100.00", "USD");
   const doubled = original.multiply(2);
   console.log(original.toString()); // Still "$100.00"
Enter fullscreen mode Exit fullscreen mode
  1. Default Configurations Matter: We carefully chose sensible defaults while allowing customization:
   // Uses currency defaults
   const simple = new Money("100", "USD");

   // Custom configuration when needed
   const custom = new Money("100", "USD", {
     decimals: 4,
     displayDecimals: 2,
     roundingMode: ROUNDING_MODE.ROUND_DOWN,
   });
Enter fullscreen mode Exit fullscreen mode
  1. Comprehensive Testing is Crucial: Our test suite covers:
    • Edge cases in arithmetic operations
    • Rounding behavior across different modes
    • Internationalization scenarios
    • Type safety validations
    • Performance benchmarks

Conclusion

Building a robust money handling library taught us valuable lessons about type safety, precision, and the importance of real-world testing. The library is now being used in production systems handling millions in transactions, proving that careful attention to detail in financial software pays off.

Key takeaways:

  • Type safety prevents costly mistakes
  • Immutability simplifies reasoning about code
  • Comprehensive testing is non-negotiable
  • Sensible defaults with flexibility for edge cases

The library is MIT licensed and maintained by Smiling Sloth. We welcome contributions and feedback from the community!

money Article's
30 articles in total
Favicon
Calculating Quarterly Taxes as a Freelancer
Favicon
Building a Type-Safe Money Handling Library in TypeScript
Favicon
Understanding 401k Hardship Withdrawal Reasons: A Comprehensive Guide
Favicon
How to earn money online?
Favicon
Lesson 4: Money at the service of the body, and the body at the service of money
Favicon
E Currency and Digital Currency Exchanger
Favicon
What is KashKick App? A Comprehensive Guide to Earning Cash Online
Favicon
Lesson 3: The root of all evil
Favicon
Lesson 2: The desire to possess
Favicon
Lesson 1: Why do we have needs?
Favicon
First Part - Undoing Our Vision of Money
Favicon
Introduction - A New Vision on Money
Favicon
Experience the Thrill of Andar Bahar: A Classic Casino Game!
Favicon
A new vision on money (intro for Dev.to)
Favicon
Increase Your Income with Affiliate Marketing
Favicon
5 Hal Tidak Bisa Dibeli Dengan Uang
Favicon
Earn more by Doing SEO
Favicon
Moving Beyond Prediction into the Realm of Trading Strategy and Simulation
Favicon
HIRE EXPERT TO RECOVER MONEY BACK..
Favicon
Ai-Money-Maker
Favicon
Fintech: The Tech Banking Sector We Should Be Grateful For
Favicon
How to Open a USD Account on Cleva
Favicon
Win Big with the Cash App $750 Gift Card Giveaway – Enter Now!
Favicon
Don't Miss Out: Cash App $750 Gift Card Giveaway for Lucky Winners!
Favicon
How do I kickstart an AI business to make money?
Favicon
Boş vaxt
Favicon
Get Free $750 Cash App Money
Favicon
Get free $750 to spend on Amazon!
Favicon
Everyone wants to become rich, but there are a few privileged ones who become rich.
Favicon
A Guide to Earning Money from Anywhere

Featured ones: