コース目次 / 第4章

ユニオンとナローイング

「失敗するかもしれない値」を number | null で表し、確認を書かされる価値を知ります。

第4章 / 全11章目安 約70分この章のゴール: 数字でない入力・空入力に日本語のエラーが出る、壊れにくい換算パネルになる

経験者の方へ: JavaScriptに自信がある方は、Aパートは流し読みで構いません。事故が起きるところと、TSにするところから精読してください。

今日は道具を鍛える日です。見た目の変化は小さめです。そのかわり、ここで身につく考え方は残りの全章で使います。峰の手前の谷なので、少し肩の力を抜いて進んでください。

今日のゴール

第3章で開けたままにした NaN の穴をふさぎます。変な入力には、画面に日本語で理由が出ます。

今日つかまえるバグ: 失敗を -1NaN で返して、確認を忘れる事故。

ミニ解説その1(JS): 失敗をどう返すか

JavaScriptには昔から「失敗したら特別な値を返す」慣習があります。indexOf は見つからないと -1Number('abc')NaN

問題は、それが失敗だと呼び出す側が忘れられることです。

失敗を握り潰した例
const amount = parseAmount(input.value); // 失敗すると -1 が返る
const yen = amount * rate;               // 確認を忘れると…
// 画面に ¥-15,000 と表示される。目で見て気づける?

-1NaN立派な数なので、そのまま計算が続いてしまいます。

Aパート: JSで書く

新しいファイル src/parse.js を作ります。失敗したら null を返す方式で書きます。

src/parse.js
export function parseAmount(text) {
  const trimmed = text.trim();
  if (trimmed === '') return null;

  const value = Number(trimmed);
  if (Number.isNaN(value)) return null;
  if (value <= 0) return null;

  return value;
}

export function toCurrency(value) {
  if (value === 'TWD' || value === 'EUR' || value === 'USD') {
    return value;
  }
  return null;
}

exchange.js から使うように直します。

src/exchange.ts+2 / -0
+import { parseAmount, toCurrency } from './parse';+ const CURRENCIES = ['TWD', 'EUR', 'USD'];

行頭の + - は変更の目印です。コピーすると、記号と削除された行を除いたこの章を終えた時点の内容が入ります。

src/exchange.ts+15 / -2
   const update = () => {-    const amount = Number(amountInput.value);-    const code = currencySelect.value;+    const amount = parseAmount(amountInput.value);+    if (amount === null) {+      result.textContent = '金額は0より大きい数字で入れてください';+      result.classList.add('error');+      return;+    }++    const code = toCurrency(currencySelect.value);+    if (code === null) {+      result.textContent = 'この通貨にはまだ対応していません';+      result.classList.add('error');+      return;+    }++    result.classList.remove('error');     const yen = convert(amount, rateFor(code));     result.textContent = `${amount.toLocaleString()} ${code} = ¥${yen.toLocaleString()}`;

行頭の + - は変更の目印です。コピーすると、記号と削除された行を除いたこの章を終えた時点の内容が入ります。

ここまでがAパートです。 空欄や abc を入れると、日本語のエラーが出るようになりました。

事故を踏む

if (amount === null) のかたまりを消してみてください。空欄にすると ¥NaN-5 と入れると ¥-24,400 のような値が平然と出ます。JavaScriptは何も言いません。戻してから次へ。

ミニ解説その2(TS): ユニオンとナローイング

parseAmount は「数を返すこともあれば、失敗して null を返すこともある」関数です。それをそのまま型に書けます。

ユニオン型
function parseAmount(text: string): number | null

| で並べたものを ユニオン型 と呼びます。「AまたはB」です。

大事なのはここからです。number | null のままでは掛け算できません。 TypeScriptが「null かもしれないでしょう」と止めます。掛け算するには、null でないことを確かめた証拠が要ります。

確かめると、そこから先は number になる
const amount = parseAmount(text); // number | null
if (amount === null) return;
// ここから下では amount は number

この「ifを書くと、その先で型が狭まる」動きを ナローイング と呼びます。

2レーンの地図でいうと、「値の世界のifが、型の世界の絞り込みを証明している」のです。呪文ではありません。

そして 第3章の種明かしです。if (el === null) return; を定型で書いてもらったあれは、まさにこのナローイングでした。querySelector の戻りが HTMLInputElement | null というユニオンだったのです。

これが効いているのは tsconfig.json"strict": true の中の strictNullChecks という設定です。これがないと、TypeScriptは null を見逃します。

リテラル型

もう1つ。文字列そのものを型にできます

リテラル型とユニオン
type Currency = 'TWD' | 'EUR' | 'USD';

「この3つのどれか」という意味です。string よりずっと狭い型です。

Bパート: TSにする

parse.jsF2parse.ts にリネームします。

parse.js → parse.ts型だけの変更+4 / -2
-export function parseAmount(text) {+export type Currency = 'TWD' | 'EUR' | 'USD';++export function parseAmount(text: string): number | null {   const trimmed = text.trim();   if (trimmed === '') return null;    const value = Number(trimmed);   if (Number.isNaN(value)) return null;   if (value <= 0) return null;    return value; } -export function toCurrency(value) {+export function toCurrency(value: string): Currency | null {   if (value === 'TWD' || value === 'EUR' || value === 'USD') {     return value;   }   return null; }

行頭の + - は変更の目印です。コピーすると、記号と削除された行を除いたこの章を終えた時点の内容が入ります。

toCurrency の中の if に注目してください。value === 'TWD' || ... と確かめたことで、TypeScriptは「ここから先の valueCurrency だ」と認めます。ナローイングが stringCurrency に狭めています。

複数のエラーを読む練習

parse.ts の戻り値の型を、わざと number に変えてみてください(| null を消す)。npm run check を打つと、エラーが複数出ます。

エラーの数は怖がらないでください。やることは1つです。
上から1個だけ直す → 保存 → もう一度 check。
1個直すと連鎖して数が減ります。10個が3個になり、3個が0個になります。独学でいちばん心が折れるのがここなので、安全な場所で先に通過しておきましょう。

確認したら | null を戻します。

わざと壊してみる

parse.ts の最後に次の1行を足してください。

src/parse.ts(この行を足す)
const test: Currency = 'GBP';

ここで赤くなるのが正解です。 Type '"GBP"' is not assignable to type 'Currency' = 「GBPはCurrencyの仲間ではありません」。存在しない通貨コードが、書いた瞬間に止まります。確認したら消します。

あなたの番

  • エラーの文言を自分の言葉にしてください。
  • | null を返す分岐を1つ自作してください。 例: 上限額チェック(1000万を超えたら null)。
  • 合格条件: 変えたあとも npm run check がエラー0。

実務メモ

「失敗は null で返して、呼び出す側に処理を強制する」——React Nativeのフォーム処理でも定石です。呼び出し側が書かされることに価値があります。

TSエラー語辞典

この章で実際に出るエラー英文と、その日本語訳です。卒業するころには全章ぶんがお助けページに集まります。

エラー英文 言っていること
`Argument of type ’number null’ is not assignable to parameter of type ‘number’`
'amount' is possibly 'null' それ、nullかもしれません。先に確かめてください
Type '"GBP"' is not assignable to type 'Currency' GBPはCurrencyの仲間ではありません

ハマったら

コールバックの中でナローイングが効かない

if で確かめたのに、addEventListener の中では赤線が戻ることがあります。理屈は深追いせず、「先に変数に取り出す」定型で回避してください。

ZIPで戻したい

このページの下には2つのZIPがあります。JS版はAパート(JSで動いた状態)まで、TS版は章末(TS化まで終わった状態)です。詰まった場所に合わせて選んでください。

解凍したら、フォルダの中で次の2つを順に打ちます。

ZIPを解凍したフォルダの中で
npm installnpm run dev

行頭の $ は「ここからコマンド」という目印です。$ は打たないでください。

丸ごと置き換えると自分で書き換えた内容が消えます。 まず src/places.ts(自分の行き先データ)と、自分で変えた文言をメモ帳に待避してから、壊れたファイルだけを差し替えてください。

こうなっていればOK

うまくいかないときは、この章のファイル一式をダウンロードして続きから進めても大丈夫です。JS版(TS化する前)TS版(章末)の2つがあります。

あと2章で、あなたのサイトに公開URLが付きます。

この章はまだ完了していません。