コース目次 / 第5章

非同期とfetch

本物の為替APIからレートを取ってきます。await忘れが赤線になる体験つき。

第5章 / 全11章目安 約105分この章のゴール: 実際の為替レートで換算され、取得日が表示される

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

この章は長めです(★)。 Aパート(JSで動く)まで進んだら、今日はそこで店じまいして構いません。TS化は明日のあなたに任せて大丈夫です。ZIPも2種類用意してあります。

今日のゴール

固定レートが 本物の為替レート になります。取得した日付も出ます。

今日つかまえるバグ: await を1つ書き忘れる事故。画面は何も言わずに止まります

ミニ解説その1(JS): あとで届く値

インターネットからデータを取るのには時間がかかります。その間、画面が固まっては困るので、JavaScriptは「あとで届くね」という引換券を先に返します。これが Promise です。

引換券を本物の中身に替えるのが await です。

const res = await fetch(url); // 届くまで待つ
const data = await res.json(); // 中身をJSONとして読む(これも待つ)

await を使う関数には async を付けます。ここに法則が1つ。

asyncは上に伝染します。 await したくなったら自分も async。その関数を呼ぶ側も await したくなって async。…と上へ伝わります。赤線が連鎖しても壊れたわけではありません。順番に async を足していけば止まります。

使うAPI

Frankfurter という為替APIを使います。鍵の登録は要りません。

URL
https://api.frankfurter.dev/v2/rates?base=JPY&quotes=EUR,TWD,USD

返ってくるのは平らな配列です。

fixtures/rates.json(録画したもの・全文)
[
  {
    "date": "2026-08-05",
    "base": "JPY",
    "quote": "EUR",
    "rate": 0.00549
  },
  {
    "date": "2026-08-05",
    "base": "JPY",
    "quote": "TWD",
    "rate": 0.20482
  },
  {
    "date": "2026-08-05",
    "base": "JPY",
    "quote": "USD",
    "rate": 0.00634
  }
]

「ググった記事とURLが違うんですが」
その通りです。世の中の記事の多くは古い api.frankfurter.app/latest と、{“rates”: {“USD”: …}} のような入れ子の形を案内しています。これはv1の形で、v2で平らな配列に変わりました
APIは変わります。だから最後に信じるのは記事ではなく公式ドキュメントと、自分の目で見た実物です。このコースのURLは実際に動作を確かめたものだけを載せています。
そしてこの「形が変わった」という実話は、第7章でもう一度、主役として戻ってきます。

Aパート: JSで書く

新しいファイル src/rates.js を作ります。

src/rates.js
const ENDPOINT = 'https://api.frankfurter.dev/v2/rates';

export async function fetchRates(quotes) {
  const url = `${ENDPOINT}?base=JPY&quotes=${quotes.join(',')}`;
  const res = await fetch(url);
  const data = await res.json();
  return data;
}

export function findRate(rows, quote) {
  return rows.find((row) => row.quote === quote);
}

気づいた方へ: 通信の失敗をまだ何も考えていません。これは手抜きではなく順番です。失敗系は第8章でまとめて回収します。今日は「うまくいく道」だけを通します。

exchange.ts(第3章で .ts にしたファイルです)を、固定レートではなく取得したレートを使うように直します。

src/exchange.ts+2 / -7
 import { parseAmount, toCurrency } from './parse';+import { fetchRates, findRate } from './rates';  const CURRENCIES = ['TWD', 'EUR', 'USD']; -function rateFor(code: string): number {-  if (code === 'TWD') return 4.88;-  if (code === 'EUR') return 182.1;-  return 157.8;-}- export function convert(amount: number, rate: number): number {   return Math.round(amount * rate); } -export function setupExchange(): void {+export async function setupExchange(): Promise<void> {   const box = document.querySelector('#exchange')!;   box.innerHTML = `

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

src/exchange.ts+1 / -0
     </label>     <p class="result" id="result"></p>+    <p class="note" id="rate-note"></p>   `;

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

src/exchange.ts+4 / -1
   const currencySelect = document.querySelector<HTMLSelectElement>('#currency');   const result = document.querySelector('#result');-  if (amountInput === null || currencySelect === null || result === null) {+  const note = document.querySelector('#rate-note');+  if (amountInput === null || currencySelect === null || result === null || note === null) {     return;   } +  const rows = await fetchRates(CURRENCIES);+   const update = () => {     const amount = parseAmount(amountInput.value);

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

src/exchange.ts+9 / -1
     } +    const row = findRate(rows, code);+    if (row === undefined) {+      result.textContent = 'この通貨のレートが取れませんでした';+      result.classList.add('error');+      return;+    }+     result.classList.remove('error');-    const yen = convert(amount, rateFor(code));+    const yen = convert(amount, 1 / row.rate);     result.textContent = `${amount.toLocaleString()} ${code} = ¥${yen.toLocaleString()}`;+    note.textContent = `${row.date} のレートで計算しています`;   };

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

保存すると、本物のレートで換算されます。開発者ツールの Network タブを開いてリロードすると、APIへの通信が1行増えるのが見えます(小出しに教えるDevToolsの第3弾です)。

ここまでがAパートです。今日はここで店じまいしてOKです。

事故を踏む

const rows = await fetchRates(CURRENCIES); から await を1つ消してください。

画面の結果欄は空のまま、何も起きません。エラーダイアログも出ませんし、開発サーバも止まりません。「壊れた」というより「沈黙した」感じです。

原因を知るには、開発者ツールのConsoleを開きます(第7章のDevTools導入の先取りですが、ここは見るだけです)。

Console に出ているもの
Uncaught (in promise) TypeError: rows.find is not a function

「rows.find は関数ではありません」。 rows の中身は、まだ引換券(Promise)そのものだからです。引換券に「見つけて」と頼んでも、そんな機能は付いていません。
JavaScriptのつらいところは、これが画面に出ないことです。Consoleを開く習慣が無ければ、原因不明の沈黙として残ります。

戻してから次へ進んでください。

Bパート: TSにする

rates.jsF2rates.ts にリネームします。レスポンスの型は、録画した実物(fixtures/rates.json)を見ながら起こします。 記憶で書かないのがコツです。

rates.js → rates.ts型だけの変更+10 / -3
+export type Rate = {+  date: string;+  base: string;+  quote: string;+  rate: number;+};+ const ENDPOINT = 'https://api.frankfurter.dev/v2/rates'; -export async function fetchRates(quotes) {+export async function fetchRates(quotes: string[]): Promise<Rate[]> {   const url = `${ENDPOINT}?base=JPY&quotes=${quotes.join(',')}`;   const res = await fetch(url);-  const data = await res.json();+  const data = (await res.json()) as Rate[];   return data; } -export function findRate(rows, quote) {+export function findRate(rows: Rate[], quote: string): Rate | undefined {   return rows.find((row) => row.quote === quote); }

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

  • Promise<Rate[]> … 「あとで Rate[] が届く」。山括弧の2回目です(1回目は querySelector<HTMLInputElement>)。
  • findRate の戻りは Rate | undefined第4章のナローイングがさっそく再利用されます。

そして await を消す事故をもう一度。今度は Consoleを開くまでもなく、書いた瞬間に赤線が出ます。

npm run check の出力
src/exchange.ts(52,26): error TS2740: Type 'Promise<Rate[]>' is missing the following properties from type 'Rate[]': length, pop, push, concat, and 29 more.

読み下すと「Promise<Rate[]> には、Rate[] にあるはずのものが足りません」。lengthpush も無い——それはまだ包みの中だからです。 await で開ければ済みます。
さっきはConsoleを開いて初めて分かったことが、今度は保存した瞬間に分かります。これがこの章のいちばんのご褒美です。

as という2枚目の仮免許

(await res.json()) as Rate[]as は、「これは Rate[] だと思って扱ってください」という宣言です。

as検査ではありません。宣言です。だから嘘がつけます。実際に届いたデータが違う形でも、as は何も言いません。
これは仮免許です。第7章で卒業式をします(なぜ危ないのかを、実際に壊しながら見ます)。

わざと壊してみる

fetchRatesasync を消してください。ここで赤くなるのが正解です。 'await' expressions are only allowed within async functions。確認したら戻します。

あなたの番

  • CURRENCIES自分の行き先の通貨に変えてください。
  • URLに &date=2026-01-15 のように足すと、その日のレートが取れます。「前回の旅行の日のレート」を出してみてください(URL組み立ての練習です)。
  • 合格条件: 変えたあとも npm run check がエラー0。

実務メモ

Reactの useEffect の中でデータを取ってくる処理は、今日書いた setupExchange とまったく同じ形をしています。

TSエラー語辞典

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

エラー英文 言っていること
Type 'Promise<Rate[]>' is missing the following properties from type 'Rate[]' それはまだ包みの中です。awaitで開けてください
TypeError: rows.find is not a function (Consoleに出る方)包みのまま中身を触ろうとしています
'await' expressions are only allowed within async functions awaitを使うなら、その関数にasyncを付けてください

ハマったら

APIに繋がらない(5分試してダメなら)

会社や学校のネットワーク、あるいは一時的な障害でfetchが通らないことがあります。ここで止まらないでください。 録画済みのデータに差し替えれば、この章は最後まで進められます。

手順1. プロジェクトの一番上(src と同じ並び)に fixtures フォルダを作り、その中に rates.json を作って、上に載せた全文をそのまま貼ります。

手順2. fetchRates の中身を、いったんこれに差し替えます(まだ .js なら src/rates.js、TS化のあとなら src/rates.ts です)。

src/rates.js(TS化後は src/rates.ts)
import rows from '../fixtures/rates.json';

export async function fetchRates(quotes) {
  return rows;
}

章が終わったら元に戻してください(戻さなくても、この先の章は進みます)。この fixtures フォルダは第9章でもう一度使いますので、消さずに置いておいてください。

日付が今日じゃない

為替市場は土日祝に閉まります。休日は直近の営業日の日付が返ります。壊れていません。

CORS という単語を見かけた

「よそのサイトのデータを勝手に読まないで」というブラウザの安全装置です。このコースで使うAPIは、鍵が不要でCORSも許可されていることを実際に確かめて選んでありますので、今は気にしなくて大丈夫です。世の中には鍵とCORSの壁があるAPIも多く、そこはコース3の入り口です。

ひとこと: 他人のサーバを借りています

このコースで使うAPIは、どちらも無料で公開してくれている他人のサーバです。Frankfurterもオープンな運営、Open-Meteoは非商用なら無料という条件で公開されています。

だから呼びすぎないのが礼儀です。この教材が「再取得は更新ボタンを押したときだけ」という作りにしているのは、そのためでもあります。自分のアプリを作るときも、同じデータを何度も取りに行かない(一度取ったら覚えておく)ことを意識してください。

ZIPで戻したい

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

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

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

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

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

こうなっていればOK

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

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

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