TypeScript:非同期処理と型
1学習の目的
- async / await に型を付けられるようになる。JS15で最大の落とし穴だった「await の書き忘れ」が、実行前にエラーとして検出されることを体験する。
- Promise.all やエラー処理に型を与え、非同期処理の結果を安全に扱えるようになる。
2基礎解説
| やりたいこと | 書き方 | 意味 |
|---|---|---|
| 非同期関数の戻り値 | async function f(): Promise<number> | 必ず Promise で包む |
| 結果を取り出す | const n: number = await f() | await で中身が出る |
| 並列実行 | await Promise.all([a(), b()]) | それぞれの型が保たれる |
| エラーの型 | catch (e) { … } | e は unknown |
- async 関数の戻り値は必ず Promise<T>。return 42 と書いても、型は Promise<number> になる。
- ⭐ await を書き忘れると型エラーになる。const n = fetchNumber(); のあと n * 2 と書けば、「Promise は掛け算できない」と止められる。JS15で [object Promise] に悩まされた問題が、ここで解決する。
- Promise.all は各要素の型を保つ。[Promise<number>, Promise<string>] を渡せば、結果は [number, string] になる。
- catch で受け取る e は unknown 型。e.message と直接書けないので、e instanceof Error で確認してから使う(TS09の型ガード)。
- ジェネリクスと組み合わせると、「どんな型でも安全に扱える非同期処理」が書ける(TS10)。
現場使用例:APIからのデータ取得、ファイルの読み込み、データベースへの問い合わせ、複数の処理の並列実行。
// 戻り値は Promise<T> になる
async function fetchPrice(): Promise<number> {
return 128000;
}
async function main() {
// ✅ await で中身を取り出す
const price = await fetchPrice(); // number
console.log(price * 2);
// ❌ await を忘れると Promise のまま → エラーになる
const bad = fetchPrice(); // Promise<number>
// console.log(bad * 2);
// → The left-hand side of an arithmetic operation must be of type 'number'...
// 並列実行(それぞれの型が保たれる)
const [n, s] = await Promise.all([
fetchPrice(), // Promise<number>
Promise.resolve("文字列"), // Promise<string>
]);
console.log(n, s); // number, string
// エラー処理(e は unknown なので確認が必要)
try {
await Promise.reject(new Error("通信失敗"));
} catch (e) {
console.log(e instanceof Error ? e.message : "不明なエラー");
}
}
main();
3基本ドリル(10問)
出力 == 84
async function getValue(): Promise
async function getValue(): Promise<number> {
return 42;
}
async function main() {
const value = await getValue();
console.log(value * 2);
}
main();
戻り値の型は Promise<number>。中身が number でも、async を付けた時点で Promise に包まれる。
A. NaN になる B. 実行前にエラーになる C. [object Promise] が出る D. 問題ない 選択★☆☆無料
解答 == B
Promise は掛け算できるか。
B
JS15で最も悩まされた落とし穴が、ここで解決する。JavaScriptなら [object Promise] が表示されて初めて気づいたが、TypeScriptは書いた時点で止めてくれる。
出力 == こんにちは、田中さん
戻り値の型は Promise
async function getName(): Promise<string> {
return "田中";
}
async function main() {
const name = await getName();
console.log(`こんにちは、${name}さん`);
}
main();
await した結果は string として扱える。テンプレートリテラルに埋め込んでも安全。
async function getCount(): ____<number> {
return 10;
}
async function main() {
console.log(await getCount());
}
main();
出力 == 10
非同期の結果を包む型。先頭大文字で7文字。
Promise
async を付けたら戻り値は必ず Promise で包む。: number と書くとエラーになる。
A. Error B. unknown C. any D. string 選択★☆☆無料
解答 == B
何が投げられるか分からない。
B
JavaScriptでは Error 以外も throw できるので、TypeScriptは unknown として扱う。e.message と書く前に確認が必要(TS09)。
出力 == 42 / 田中
const [n, s] = await Promise.all([getNum(), getName()]); と書く。それぞれの型が保たれる。
async function getNum(): Promise<number> {
return 42;
}
async function getName(): Promise<string> {
return "田中";
}
async function main() {
const [num, name] = await Promise.all([getNum(), getName()]);
console.log(`${num} / ${name}`);
}
main();
Promise.all は要素ごとの型を保つ。num は number、name は string として正しく推論される。
async function main() {
try {
throw new Error("失敗しました");
} catch (e) {
console.log(e ____ Error ? e.message : "不明");
}
}
main();
出力 == 失敗しました
「このクラスのインスタンスか」を判定する演算子。10文字。
instanceof
unknown のままでは message にアクセスできない。instanceof で Error だと確認して初めて、そのプロパティが使える。
A. 自動変換される B. エラーになる C. Promise<string> になる D. 問題ない 選択★★☆無料
解答 == B
宣言した型と返す値が一致しているか。
B
Promise の中身の型もチェックされる。「数値を返す」と宣言したのに文字列を返せば、その場でエラーになる。
出力 == ノートPC:128,000円
戻り値の型は Promise
type Product = { name: string; price: number };
async function fetchProduct(): Promise<Product> {
return { name: "ノートPC", price: 128000 };
}
async function main() {
const p = await fetchProduct();
console.log(`${p.name}:${p.price.toLocaleString()}円`);
}
main();
await した結果は Product として扱える。p.nmae のような打ち間違いも、その場で検出される。
出力2行。42 / あ
async function wrap
async function wrap<T>(value: T): Promise<T> {
return value;
}
async function main() {
console.log(await wrap(42));
console.log(await wrap("あ"));
}
main();
ジェネリクスと非同期の組み合わせ。入れた型がそのまま出てくるので、any を使わずに汎用的な非同期処理が書ける。
4実践シナリオ(5問)
出力2行が完全一致
戻り値の型は Promise
type Product = { name: string; price: number };
async function fetchProduct(id: number): Promise<Product | null> {
return id === 1 ? { name: "ノートPC", price: 128000 } : null;
}
async function main() {
for (const id of [1, 99]) {
const p = await fetchProduct(id);
console.log(
p ? `${p.name}:${p.price.toLocaleString()}円` : "該当なし"
);
}
}
main();
Promise<Product | null> で「見つからない可能性」を型で表現している。呼び出し側は null チェックを迫られるので、確認漏れが起きない。
出力 == ノートPC:在庫5個
Promise.all の結果を分割代入で受け取る。型は [string, number] になる。
async function fetchName(): Promise<string> {
return "ノートPC";
}
async function fetchStock(): Promise<number> {
return 5;
}
async function main() {
const [name, stock] = await Promise.all([fetchName(), fetchStock()]);
console.log(`${name}:在庫${stock}個`);
}
main();
それぞれの型が正しく保たれている。name は string、stock は number——順序を取り違えれば型エラーになる。
出力 == エラー:通信に失敗しました
catch (e) の e は unknown。instanceof Error で確認する。
async function fetchData(): Promise<string> {
throw new Error("通信に失敗しました");
}
async function main() {
try {
await fetchData();
} catch (e) {
const message = e instanceof Error ? e.message : "不明なエラー";
console.log(`エラー:${message}`);
}
}
main();
e は unknown なので、そのまま message にアクセスできない。TS09の型ガードが、エラー処理の場面でも必要になる。
出力2行が完全一致
型は Promise<{ ok: true; data: T } | { ok: false; error: string }>。try/catch で包む。
type Result<T> =
| { ok: true; data: T }
| { ok: false; error: string };
async function safe<T>(promise: Promise<T>): Promise<Result<T>> {
try {
return { ok: true, data: await promise };
} catch (e) {
return {
ok: false,
error: e instanceof Error ? e.message : "不明なエラー",
};
}
}
async function main() {
const a = await safe(Promise.resolve(42));
console.log(a.ok ? `成功:${a.data}` : `失敗:${a.error}`);
const b = await safe(Promise.reject(new Error("通信エラー")));
console.log(b.ok ? `成功:${b.data}` : `失敗:${b.error}`);
}
main();
例外を「値」に変換している。try/catch を毎回書かずに、if (result.ok) で分岐できるようになる——TS09のタグ付きユニオンとTS10のジェネリクスの組み合わせ。
出力4行が完全一致
for...of の中で try/catch する。カウンタをループの外に用意する。
async function fetchName(id: number): Promise<string> {
if (id === 2) {
throw new Error("取得失敗");
}
return id === 1 ? "ノートPC" : "マウス";
}
async function main() {
let ok = 0;
let ng = 0;
for (const id of [1, 2, 3]) {
try {
const name = await fetchName(id);
console.log(`[${id}] ${name}`);
ok++;
} catch (e) {
const message = e instanceof Error ? e.message : "不明";
console.log(`[${id}] ${message}`);
ng++;
}
}
console.log(`成功${ok}件 / 失敗${ng}件`);
}
main();
try/catch をループの中に置くのが要点(JS15と同じ)。外に置くと最初の失敗で全体が止まる。
5仕上げ課題
擬似API(すべて async 関数として実装)
・fetchProduct(id): Promise<Product> … id=3 は例外を投げる
データ:1→ノートPC 128000/2→マウス 3200/4→モニター 45000
・fetchStock(id): Promise<number> … 1→5, 2→0, 4→3
型:type Product = { id: number; name: string; price: number };
処理
① id 1, 2, 3, 4 を順に処理
② 各IDで商品と在庫を Promise.all で並列取得
③ 成功なら「[1] ノートPC:128,000円(在庫5個)」、在庫0なら末尾に「 ※品切れ」
④ 失敗なら「[3] エラー:商品が見つかりません」
⑤ 最後に成功・失敗件数と合計金額を出力
期待される出力(6行)
=== 商品情報の取得 ===
[1] ノートPC:128,000円(在庫5個)
[2] マウス:3,200円(在庫0個) ※品切れ
[3] エラー:商品が見つかりません
[4] モニター:45,000円(在庫3個)
成功3件 / 失敗1件 / 合計176,200円
出力6行が完全一致
Record
type Product = { id: number; name: string; price: number };
const PRODUCTS: Record<number, Omit<Product, "id">> = {
1: { name: "ノートPC", price: 128000 },
2: { name: "マウス", price: 3200 },
4: { name: "モニター", price: 45000 },
};
const STOCKS: Record<number, number> = { 1: 5, 2: 0, 4: 3 };
async function fetchProduct(id: number): Promise<Product> {
const found = PRODUCTS[id];
if (!found) {
throw new Error("商品が見つかりません");
}
return { id, ...found };
}
async function fetchStock(id: number): Promise<number> {
return STOCKS[id] ?? 0;
}
async function main() {
console.log("=== 商品情報の取得 ===");
let ok = 0;
let ng = 0;
let total = 0;
for (const id of [1, 2, 3, 4]) {
try {
const [product, stock] = await Promise.all([
fetchProduct(id),
fetchStock(id),
]);
const mark = stock === 0 ? " ※品切れ" : "";
console.log(
`[${id}] ${product.name}:${product.price.toLocaleString()}円(在庫${stock}個)${mark}`
);
total += product.price;
ok++;
} catch (e) {
const message = e instanceof Error ? e.message : "不明なエラー";
console.log(`[${id}] エラー:${message}`);
ng++;
}
}
console.log(
`成功${ok}件 / 失敗${ng}件 / 合計${total.toLocaleString()}円`
);
}
main();
JS15の仕上げ課題と同じ処理を、型付きで書き直した。違いは3つある。
第一に、await の書き忘れが起こりえない。const [product, stock] = Promise.all([…]) と await を落とせば、分割代入の時点でエラーになる。JS15では [object Promise] が表示されて初めて気づいていた。
第二に、catch (e) の e が unknown であること。e.message と直接書けないので、「Error 以外が投げられる可能性」を必ず考えさせられる。実際、JavaScriptでは文字列やオブジェクトも throw できる。
第三に、Record<number, Omit<Product, "id">> という型だ。TS13で学んだユーティリティ型を使い、「idはキーとして持つので、値には含めない」という構造を正確に表現している。
非同期処理は「いつ結果が来るか分からない」という不確実さを扱う。TypeScriptは、その不確実さを型として明示し、扱い忘れを防いでくれる。
次章では外部データの型付けを学ぶ。実際のAPIから来る「型が保証されないデータ」を、安全に扱う方法を扱う。