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 { Controller } from '@nestjs/common'; import { MessagePattern, Payload,
Manual RabbitMQ acks in a NestJS controller
microservices
message-queue
acknowledgement
Intermediate
8 steps
typescript
type LazyImageOptions = { rootMargin?: string; loadedClass?: string; };
Lazy-loading images with IntersectionObserver
intersectionobserver
lazy-loading
performance
Intermediate
7 steps
typescript
type DeepPartial<T> = T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T;
Type-safe deep merge in TypeScript
recursion
conditional-types
mapped-types
Advanced
7 steps
php
<?php namespace App\Models;
Debouncing Eloquent jobs in Laravel
debounce
model-events
queued-jobs
Advanced
7 steps
typescript
import { Component, ChangeDetectionStrategy } from '@angular/core'; @Component({ selector: 'app-card',
Multi-slot content projection in Angular
content-projection
components
templates
Intermediate
7 steps
typescript
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository, DeepPartial } from 'typeorm'; import { User } from './entities/user.entity';
Building a CRUD service in NestJS
crud
dependency-injection
repository-pattern
Intermediate
7 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.