typescript
55 lines · 9 steps
Caching Intl.NumberFormat for currency
A memoized factory reuses expensive Intl.NumberFormat instances keyed by their formatting options.
Explained by
highlit
1type CurrencyFormatOptions = {
2 locale?: string;
3 currency: string;
4 showDecimals?: boolean;
5 compact?: boolean;
6};
7
8const formatterCache = new Map<string, Intl.NumberFormat>();
9
10function getFormatter(options: CurrencyFormatOptions): Intl.NumberFormat {
11 const {
12 locale = "en-US",
13 currency,
14 showDecimals = true,
15 compact = false,
16 } = options;
17
18 const cacheKey = `${locale}:${currency}:${showDecimals}:${compact}`;
19 const cached = formatterCache.get(cacheKey);
20 if (cached) return cached;
21
22 const formatter = new Intl.NumberFormat(locale, {
23 style: "currency",
24 currency,
25 notation: compact ? "compact" : "standard",
26 minimumFractionDigits: showDecimals ? 2 : 0,
27 maximumFractionDigits: showDecimals ? 2 : 0,
28 });
29
30 formatterCache.set(cacheKey, formatter);
31 return formatter;
32}
33
34export function formatCurrency(
35 amount: number,
36 options: CurrencyFormatOptions,
37): string {
38 if (!Number.isFinite(amount)) {
39 throw new RangeError(`Cannot format non-finite amount: ${amount}`);
40 }
41 return getFormatter(options).format(amount);
42}
43
44export function formatCurrencyParts(
45 amount: number,
46 options: CurrencyFormatOptions,
47): { symbol: string; value: string } {
48 const parts = getFormatter(options).formatToParts(amount);
49 const symbol = parts.find((p) => p.type === "currency")?.value ?? "";
50 const value = parts
51 .filter((p) => p.type !== "currency" && p.type !== "literal")
52 .map((p) => p.value)
53 .join("");
54 return { symbol, value };
55}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Constructing Intl.NumberFormat is costly, so caching instances by their config avoids repeated setup.
- 2A cache key built from every option that changes behavior keeps memoized results correct.
- 3formatToParts lets you split a formatted number into its symbol and numeric pieces.
Related explainers
typescript
import { registerLocaleData } from '@angular/common'; import localeFr from '@angular/common/locales/fr'; import localeFrExtra from '@angular/common/locales/extra/fr'; import localeDe from '@angular/common/locales/de';
Locale-aware bootstrapping in Angular
i18n
localization
dependency-injection
Intermediate
8 steps
typescript
import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import * as Joi from 'joi';
Validating env config at boot in NestJS
configuration
schema-validation
environment-variables
Intermediate
8 steps
typescript
import { Inject, Injectable, Logger } from '@nestjs/common'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Cache } from 'cache-manager'; import { InjectRepository } from '@nestjs/typeorm';
A cache-aside country lookup in NestJS
cache-aside
dependency-injection
batch-lookup
Intermediate
8 steps
typescript
import { Injectable, effect, signal, computed } from '@angular/core'; interface Preferences { theme: 'light' | 'dark';
A signal-based preferences store in Angular
signals
state-management
persistence
Intermediate
7 steps
javascript
import { useState, useEffect, useCallback, useRef } from 'react'; const cache = new Map(); const inflight = new Map();
Building a stale-while-revalidate hook in React
caching
request-deduplication
custom-hooks
Advanced
10 steps
php
<?php namespace App\Services;
Building a cached daily leaderboard in Laravel
caching
aggregation
eager-loading
Intermediate
9 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/caching-intl-numberformat-for-currency-explained-typescript-1e37/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.