zukucode
主にWEB関連の情報を技術メモとして発信しています。

TypeScript 金額の表示ルールをまとめて画面ごとの差を減らす

ある画面では1,000円、別の画面ではJPY 1000のように、金額の表示がそろっていないことがあります。

画面ごとに桁区切りや通貨名を付けると、表示言語を増やすときや小数の扱いを変えるときに、修正箇所が散らばります。

今回は、金額を表示するルールを小さな関数へまとめる方法を紹介します。通貨換算や金額の計算ではなく、保存された値を文字列として見せる処理です。

金額・通貨・表示言語を分ける

金額の表示に必要な情報を分けて考えます。

情報例意味
金額1234.5計算済みの数値
通貨USD何の通貨か
表示言語ja-JPどの表示形式を使うか

日本語の画面で表示するからといって、米ドルの金額が日本円になるわけではありません。通貨と言語を別々に受け取るようにします。

Intl.NumberFormatへ表示を任せる

formatMoney.ts
export type Currency = 'JPY' | 'USD';

export const formatMoney = (
  amount: number | null | undefined,
  currency: Currency,
  locale: string,
): string => {
  if (amount === null || amount === undefined) return '—';
  if (!Number.isFinite(amount)) {
    throw new Error('有限の金額が必要です。');
  }

  return new Intl.NumberFormat(locale, {
    style: 'currency',
    currency,
    currencyDisplay: 'code',
  }).format(amount);
};

今回は対応通貨を日本円と米ドルに限定し、通貨記号ではなくコードを表示する例にしています。記号を使いたい場合はcurrencyDisplayの方針を変更します。

Intl.NumberFormatは、言語や通貨に応じて桁区切り、小数桁、通貨の配置などを整えます。通貨の小数桁に応じた既定値については、MDNのIntl.NumberFormatの説明を参照してください。

使用例
import { formatMoney } from './formatMoney';

console.log(formatMoney(1234.5, 'USD', 'en-US'));
console.log(formatMoney(1234, 'JPY', 'ja-JP'));
console.log(formatMoney(0, 'JPY', 'ja-JP'));
console.log(formatMoney(undefined, 'JPY', 'ja-JP')); // —

表示に含まれる空白などは実行環境の言語データによって違う場合があります。文字列をそのまま通信データやIDとして利用しないようにします。

0円と未設定を区別する

if (!amount)と書くと、0も未設定と同じ扱いになります。

無料の商品や残高0は、有効な金額です。今回の関数はnullとundefinedだけを未設定として扱います。

NaNや無限大は未設定へ読み替えず、エラーにしています。データに問題があるのに、画面では単に金額がないように見えてしまうことを避けるためです。実際の画面では、このエラーをどう表示するかも決めます。

表示の丸めを計算へ使わない

通貨の表示では小数が丸められる場合があります。たとえば、日本円の通常の通貨表示では小数桁が表示されません。

この丸めは、画面の見せ方です。税計算や端数処理の業務ルールを実装したことにはなりません。

処理を分ける例
元データ → 業務ルールで金額を計算 → 金額を保存
                                      ↓
                              表示用の文字列に変換

表示文字列からカンマや通貨記号を削って、再び計算用の数値へ戻す使い方は避けます。計算用の値を別に持ちます。

表示関数を作っても、数値そのものの精度は増えません。

この例はnumberで扱える範囲の金額が渡される前提です。大きな金額や厳密な小数計算が必要なら、整数の最小通貨単位や十進数を扱う方式など、保存と計算の設計を別に決めます。

単位の違いを共通関数で推測しない

APIが12345を返したとき、それが123.45ドル分のセントなのか、12345ドルなのかは、数値だけではわかりません。

今回の関数は、円なら円、米ドルならドルという主単位の金額を受け取ります。最小単位の整数を受け取るAPIなら、単位を把握した変換処理を通してから表示します。

通貨によって桁が異なるので、すべての金額を一律に100で割る処理を共通関数へ入れないようにします。

共通化する表示と個別の表示を分ける

通常の商品価格と、為替レートのように細かい小数桁が必要な数値は、同じ表示ルールとは限りません。

共通関数に多数の真偽値を追加して無理に合わせるより、formatMoneyとformatExchangeRateのように用途を分ける方が理解しやすい場合があります。

また、一覧の全行で毎回フォーマッターを作る費用が気になる場合は、言語と通貨ごとに再利用する方法があります。ただし、最初から無制限のキャッシュを作る必要はありません。実際の件数や処理時間を見て判断します。

確認するのは桁区切りだけではない

入力確認する内容
0未設定の記号にならない
null、undefined未設定の表示になる
日本円と米ドル通貨コードと小数桁の方針が合う
負の金額返金などで使う場合の表示が意図どおりか
NaN異常な入力として検出する
同じ金額で言語を変える数値は変えず、表示形式が変わる

見た目の変換をまとめると、表示ルールの変更箇所を減らせます。金額の単位や計算まで推測させず、関数が受け取る値の意味を先に決めておくことが大切です。


関連記事