C
TypeScript/非同期/Lesson 03

Promise<T> — TypeScript で非同期を型安全に

30分·theory
このチャプター
3/7
TypeScript

Promise — TypeScript で非同期を型安全に

💡 なぜ学ぶべきか — JS Promise の一歩先へ

🎯 JS の Promise は `.then(user => ...)` の中で `user` が何であるかを知りません — IDE の自動補完はなく、タイポはランタイムになって初めて爆発します。
💼 TS の `Promise` は「中に入る値の型」を `T` の位置に固定します — IDE が事前に教えてくれ、誤った使い方はコンパイル時に拒否されます。
実務の API 関数の 90% が Promise を返します。型が固定された Promise は呼び出し側コードの信頼性を決定します。
🔗 `async/await` や `Promise.all` といったツールも、型が固定されて初めて真の価値が生まれます(`Promise.all` はタプル型まで推論します)。
🏢 실무에서는
Next.js や React の Server Component、Server Action、API ルートはすべて `async` 関数です。戻り値の型を明示しないと、呼び出し側で毎回 `as User` のような強制キャストが発生し、それが積み重なると型システムが意味を失います。実務では `Promise`、`Promise`、`Promise` のように戻り値の型を明示することが基本です。

Promise — ジェネリクスで「格納される値」の型を固定する

Promise の T とは何か

JS Promise は「後から届く値を約束するオブジェクト」です。TS Promise はさらに一歩進んで、「どの型の値が届くか」までコンパイラに伝えるものです。

ts
Promise<string>   // 後から string が届く
Promise<User>     // 後から User オブジェクトが届く
Promise<User[]>   // 後から User の配列が届く
Promise<void>     // いずれ完了するが、受け取る値はない

T を固定すると何が変わるか

ts
const orderPizza = (): Promise<string> => { ... };

orderPizza().then((pizza) => {
  //              ^^^^ ← IDE はここで string と推論
  pizza.toUpperCase(); // ✅ string のメソッド、補完が効く
  pizza.toFixed(2);    // ❌ string に toFixed はない — コンパイル拒否
});

JS であれば pizza.toFixed(2) はコンパイルを通過 → 実行時に TypeError → テストが運よく捕まえるか、運悪ければプロダクションで発生します。

async 関数の戻り値の型は自動的に Promise でラップされる

ts
async function getUser(): Promise<User> {
  return { id: 1, name: 'ホン・ギルドン' }; // ← User を return しているが
}                                    //   戻り値の型は Promise<User>

async キーワードが付いた関数は必ず Promise を返します。 そのため戻り値の型には Promise<...> と書く必要があります(または推論に任せます)。

明示的な型 vs. 推論

実務では両方使います:

ts
// 明示 — 関数シグネチャを読むだけで意図がわかる (公開 API に推奨)
const fetchUser = (id: number): Promise<User> => { ... };

// 推論 — 短いコールバックや内部ヘルパーに適している
const delay = (ms: number) =>
  new Promise<void>((r) => setTimeout(r, ms));
// 戻り値の型: Promise<void> と推論される

Promise.all はタプル型まで推論する

ts
const [pizza, cola, fries] = await Promise.all([
  order('🍕'),  // Promise<string>
  order('🥤'),  // Promise<string>
  order('🍟'),  // Promise<string>
]);
// 👆 TS は [string, string, string] のタプルとして推論
// 型が混在する場合は [string, number, User] のように正確に捕捉される

JS では結果の配列を受け取っても各要素の型は不明です。TS は各位置の型を保持します。

💻 🅰️ 従来の JS 方式 (型情報なし)
// ============================================
// ❌ JS 方式 — IDE は user, orders, items の型を知らない
// ============================================

function fetchUser(id) {
  return new Promise((resolve) => {
    setTimeout(() => resolve({ id, name: 'ホン・ギルドン' }), 300);
  });
}

function fetchOrders(uid) {
  return new Promise((resolve) => {
    setTimeout(() => resolve([{ id: 1, item: '本' }]), 300);
  });
}

fetchUser(42)
  .then((user) => {
    // user.name?  user.id?  user.email?  IDE の自動補完なし
    console.log(user);
    return fetchOrders(user.id);
  })
  .then((orders) => {
    // orders が配列なのかオブジェクトなのか IDE は知らない
    // orders[0].item のタイプミスは実行時にのみ発見される
    console.log(orders);
  });

// 危険な点:
// - user.naem (タイプミス) → コンパイルは通過、実行時に undefined
// - orders.item   (配列なのにオブジェクトのようにアクセス) → 実行時に undefined
// - fetchUser('42') (id に文字列) → コンパイルは通過、動作は奇妙にずれる
💻 🅱️ TypeScript 方式 (Promise · interface · ジェネリクス)
// ============================================
// ✅ TS方式 — 型が明示されIDEが各ステップを支援
// 実行: tsx promise-demo.ts
// ============================================

// 例1: 最もシンプルなPromise
const ピザ注文 = (): Promise<string> => {
  //              ↑ 戻り値の型: Promise<string>
  return new Promise<string>((resolve) => {
    //                ↑ Promiseが保持する値の型
    console.log('📞 注文電話をかける');
    setTimeout(() => resolve('🍕'), 1000);
  });
};

ピザ注文().then((ピザ: string) => {
  //                ↑ 受け取る値の型
  console.log('受け取り:', ピザ);
});

// 例2: チェイニング (型推論の活用)
const 注文 = (食べ物: string, 時間: number): Promise<string> =>
  new Promise<string>((resolve) => setTimeout(() => resolve(食べ物), 時間));

注文('🍕 ピザ', 500)
  .then((食べ物) => {              // 自動推論: string
    console.log('1️⃣ 受け取り:', 食べ物);
    return 注文('🥤 コーラ', 500);
  })
  .then((食べ物) => {
    console.log('2️⃣ 受け取り:', 食べ物);
    return 注文('🍟 ポテト', 500);
  })
  .then((食べ物) => console.log('3️⃣ 受け取り:', 食べ物));

// 例3: 失敗処理 (エラー型を明示)
const 注文する = (成功するか: boolean): Promise<string> => {
  return new Promise<string>((resolve, reject) => {
    setTimeout(() => {
      if (成功するか) resolve('🍕 ピザ到着!');
      else reject(new Error('😭 店が閉まっている'));
    }, 500);
  });
};

注文する(false)
  .then((ピザ) => console.log('成功:', ピザ))
  .catch((エラー: Error) => console.log('❌ 失敗:', エラー.message));

// 例4: 実務コード (interfaceで型定義)
interface User {
  id: number;
  name: string;
}
interface Order {
  id: number;
  item: string;
}

const wait = (ms: number): Promise<void> =>
  new Promise((r) => setTimeout(r, ms));

const fetchUser = (id: number): Promise<User> =>
  wait(300).then(() => ({ id, name: '山田太郎' }));

const fetchOrders = (uid: number): Promise<Order[]> =>
  wait(300).then(() => [{ id: 1, item: '本' }]);

fetchUser(42)
  .then((user: User) => {
    // user.name?  user.id?  ← IDE自動補完OK
    // user.naem  ← コンパイルが即座に拒否 (この行に赤線)
    return fetchOrders(user.id);
  })
  .then((orders: Order[]) => {
    // orders[0].item ← 推論される
  });

// 例5: async/await (TS推奨方式)
const 作業 = (名前: string, ms: number): Promise<string> =>
  new Promise((r) => setTimeout(() => r(名前), ms));

const 実行 = async (): Promise<void> => {
  const a: string = await 作業('A', 200);
  const b: string = await 作業('B', 200);
  const c: string = await 作業('C', 200);
  console.log(a, b, c);
};

// 例6: Promise.all — タプル型で推論
const [ピザ, コーラ, ポテト]: [string, string, string] = await Promise.all([
  注文('🍕', 1000),
  注文('🥤', 1000),
  注文('🍟', 1000),
]);
// 👆 TSは位置ごとの型を保持 — 混ざると [string, number, User] のように正確に

// 例7: ジェネリックPromiseヘルパー (どんな型でも受け取る)
const delay = <T>(value: T, ms: number): Promise<T> =>
  new Promise((r) => setTimeout(() => r(value), ms));

const 数字: number = await delay(42, 300);              // T = number
const 文字列: string = await delay('こんにちは', 300);          // T = string
const オブジェクト: { name: string } = await delay({ name: '山田太郎' }, 300);

💡 💡 JS ↔ TS の核心的な違い

1. new Promise() には必ず <T> を固定せよ

ts
new Promise<string>((resolve) => ...) // resolve が string のみ受け取るよう強制
new Promise((resolve) => ...)         // T = unknown として推論される (危険)

2. async 関数の戻り値の型は明示したほうがよい

公開 API 関数はシグネチャを見るだけで意図が伝わるべきです。

ts
async function getUser(id: number): Promise<User> { ... }

3. Promise.all の結果はタプルで受け取れ

ts
const [user, orders] = await Promise.all([fetchUser(1), fetchOrders(1)]);
// user: User, orders: Order[] ← 自動

4. catch のエラーは unknown (TS 4.4+)

ts
try { ... } catch (err) {
  if (err instanceof Error) console.log(err.message); // ✅ 型の絞り込み
  // err.message に直接アクセス ← ❌ unknown には message プロパティがない
}

5. ジェネリクスのヘルパーは一度書けば永続的に使える

ts
const delay = <T>(value: T, ms: number): Promise<T> =>
  new Promise((r) => setTimeout(() => r(value), ms));
// number, string, User など何でも受け取れる — 呼び出し元で型が保持される

⚡ 実際に試してみよう — Promise (型を取り除いた実行バージョン)

上の 🅱️ TS コードから**型注釈だけを取り除いた実行可能バージョン**です。実行時の動作は同じです — 違いは IDE がミスを捕まえるかどうか、それとも実行時に発覚するかどうかです。 💡 本物の TS コンパイラで試したい場合は → 上の 🅱️ コードを [TypeScript Playground](https://www.typescriptlang.org/play) に貼り付けてください。
✏️ JS 코드
📟 コンソール出力
▶ 実行ボタンを押してください
⚠️ ブラウザのサンドボックスで実行 — console.log()のみ対応、alert/fetchは不可

確認クイズ

TypeScript において `Promise<User>` の `<User>` が意味するものは何ですか?
💡 `Promise<T>` の `T` は **resolve されるときに渡される値の型** です。そのため、`.then((value) => ...)` の `value` が自動的に `T` として推論されます。reject 側のエラーは TS の型システムの限界により別途表記がなく、`catch` で `unknown` として受け取り、型を絞り込んで使う必要があります。
Promise<T> — JS vs TS の違い - TypeScript