コース目次 / 第3章

関数とDOMの型

入力欄の値は必ず文字列。その頻出事故を型で受け止めます。

第3章 / 全11章目安 約85分この章のゴール: 金額と通貨を選ぶと円に換算される(レートはまだ固定)

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

今日のゴール

旅費の換算パネルができます。金額を入れて通貨を選ぶと、円で表示されます。

今日つかまえるバグ: 入力欄の値に数を足すと、文字としてつながってしまう事故。

ミニ解説その1(JS): 入力欄の値は、いつでも文字列

これはJavaScript最頻出の事故です。

// 入力欄に 5000 と打ったとき
const amount = input.value;
console.log(amount + 100); // 5100 ではなく "5000100"

input.value は、見た目が数字でも 必ず文字列 です。だから + が「足し算」ではなく「連結」になります。

直すには入口で数に変える必要があります。Number('5000')5000 を返します。ただし Number('abc')NaN を返します。この穴は今日はふさぎません(第4章の宿題です)。

Aパート: JSで書く

新しいファイル src/exchange.js を作ります。換算のロジックと、そのパネルのUIをここにまとめます。

src/exchange.js
const CURRENCIES = ['TWD', 'EUR', 'USD'];

function rateFor(code) {
  if (code === 'TWD') return 4.88;
  if (code === 'EUR') return 182.1;
  return 157.8;
}

export function convert(amount, rate) {
  return Math.round(amount * rate);
}

export function setupExchange() {
  const box = document.querySelector('#exchange');
  box.innerHTML = `
    <label class="field">
      金額(現地通貨)
      <input id="amount" type="text" value="1000" />
    </label>
    <label class="field">
      通貨
      <select id="currency">
        ${CURRENCIES.map((code) => `<option value="${code}">${code}</option>`).join('')}
      </select>
    </label>
    <p class="result" id="result"></p>
  `;

  const amountInput = document.querySelector('#amount');
  const currencySelect = document.querySelector('#currency');
  const result = document.querySelector('#result');
  if (amountInput === null || currencySelect === null || result === null) {
    return;
  }

  const update = () => {
    const amount = Number(amountInput.value);
    const code = currencySelect.value;
    const yen = convert(amount, rateFor(code));
    result.textContent = `${amount.toLocaleString()} ${code} = ¥${yen.toLocaleString()}`;
  };

  amountInput.addEventListener('input', update);
  currencySelect.addEventListener('change', update);
  update();
}

toLocaleString() は数字を 19,800 のように3桁区切りにしてくれます。ひと手間でぐっとそれらしくなります。

src/main.ts から呼びます。

src/main.ts+1 / -0
 import './style.css'; import { places } from './places';+import { setupExchange } from './exchange';  const TRIP_DATE = '2026-09-19';

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

src/main.ts+2 / -0
   )   .join('');++setupExchange();

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

ここまでがAパートです。

事故を踏む

convert に渡す前の値をわざと文字列のままにしてみてください。

src/exchange.js(わざと壊す)
const amount = amountInput.value; // Number() を外す

¥5000100 のような、桁のおかしい数字が出ます。エラーは出ません。戻してから次へ。

ミニ解説その2(TS): 画面の部品にも型がある

document.querySelector('#amount') が返すのは「HTMLの部品」ですが、TypeScriptは どの種類の部品か までは知りません。だから .value を読もうとすると「そんな持ち物は無い」と言われます。

そこで種類を伝えます

書き方
const amountInput = document.querySelector<HTMLInputElement>('#amount');

山括弧 <> は「取ってくる部品の種類を伝える書き方」です。いまはこの理解で十分です。この山括弧は第5章・第9章でまた出てきて、第9章で自分で作れるようになります

もう1つ。querySelector見つからなければ null を返します。だから型は HTMLInputElement | null です。触る前に確かめないと赤線が出ます。

対処は定型で覚えてください
if (el === null) return; ← これだけ。
なぜこう書くのかは第4章で分かります。いまは理屈を飲み込まなくて大丈夫です。
そしてここで ! は使いません。第1章の仮免許は #greeting の1箇所限定です。

Bパート: TSにする

exchange.jsF2exchange.ts にリネームします。

exchange.js → exchange.ts型だけの変更+6 / -6
 const CURRENCIES = ['TWD', 'EUR', 'USD']; -function rateFor(code) {+function rateFor(code: string): number {   if (code === 'TWD') return 4.88;   if (code === 'EUR') return 182.1;   return 157.8; } -export function convert(amount, rate) {+export function convert(amount: number, rate: number): number {   return Math.round(amount * rate); } -export function setupExchange() {-  const box = document.querySelector('#exchange');+export function setupExchange(): void {+  const box = document.querySelector('#exchange')!;   box.innerHTML = `     <label class="field">       金額(現地通貨)       <input id="amount" type="text" value="1000" />     </label>     <label class="field">       通貨       <select id="currency">         ${CURRENCIES.map((code) => `<option value="${code}">${code}</option>`).join('')}       </select>     </label>     <p class="result" id="result"></p>   `; -  const amountInput = document.querySelector('#amount');-  const currencySelect = document.querySelector('#currency');+  const amountInput = document.querySelector<HTMLInputElement>('#amount');+  const currencySelect = document.querySelector<HTMLSelectElement>('#currency');   const result = document.querySelector('#result');   if (amountInput === null || currencySelect === null || result === null) {     return;   }    const update = () => {     const amount = Number(amountInput.value);     const code = currencySelect.value;     const yen = convert(amount, rateFor(code));     result.textContent = `${amount.toLocaleString()} ${code} = ¥${yen.toLocaleString()}`;   };    amountInput.addEventListener('input', update);   currencySelect.addEventListener('change', update);   update(); }

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

そして Number() を外す事故をもう一度。今度は 書いた瞬間に赤線 が出ます。

npm run check の出力
src/exchange.ts(39,25): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.

convert は数を受け取る関数なのに、文字を渡しましたね」という指摘です。画面を見に行く前に、机の上で捕まりました。

わざと壊してみる

if (amountInput === null ...) の行をまるごと消してください。ここで赤くなるのが正解です。 Object is possibly 'null' = 「それ、無いかもしれませんよ」。確認したら戻します。

NaNという、型が防げない穴

金額欄に abc と打ってみてください。¥NaN と出ます。

npm run check は通ります。なぜなら NaN は number だからです。型は「数である」ことは保証しますが、「まともな数である」ことまでは保証しません。

型が防げないものがある。これはTypeScriptの限界ではなく、正しい理解です。この穴は第4章でふさぎます。今日はわざと開けたままにしておきます。

あなたの番

  • CURRENCIESrateFor に、自分の行き先の通貨を1つ足してください(レートは検索して、だいたいの数字で構いません)。
  • 手数料の関数を1本自分で書いてみてください。 引数と戻り値の注釈を自分で書く最初の機会です。
ヒント
function withFee(yen: number, feeRate: number): number {
  return Math.round(yen * (1 + feeRate));
}
  • 合格条件: 変えたあとも npm run check がエラー0。

実務メモ

React Nativeの TextInputonChangeText が渡してくるのも文字列です。入口で数値化する作法はまったく同じです。

TSエラー語辞典

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

エラー英文 言っていること
Object is possibly 'null' それ、無いかもしれませんよ
Property 'value' does not exist on type 'Element' ただの部品には value がありません。種類を伝えてください

ハマったら

possibly ‘null’ の赤線が大量に出て心が折れそう

この章でいちばん多い相談です。理屈は第4章まで待ってください。 いまは if (... === null) return; を機械的に足すだけで通ります。理解より先に手が慣れて構いません。

ZIPで戻したい

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

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

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

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

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

こうなっていればOK

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

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

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