C
TypeScript/非同期/Lesson 04

async / await — 戻り値の型を明示すると呼び出し元が自動推論される

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

async / await — 戻り値の型を明示すると呼び出し元が自動推論される

💡 なぜ学ぶべきか? — async関数のシグネチャはそのまま契約書

🎯 JSのasync関数は、戻り値の型を知るために関数本体をすべて読まなければなりません。
💼 TSのasync関数は、シグネチャに`Promise`のように明記しておけば、呼び出し側は本体を読まなくても安全に使用できます。
`await`の結果の型は自動的に推論されます — 一度明記した型はawaitチェーンの末端まで伝播します。
🔗 try/catchの`err`はTS 4.4以降`unknown`です。`.message`にアクセスするには`instanceof Error`のような型の絞り込みが必要です。
🏢 실무에서는
Next.jsのServer Component、Server Action、API Routeハンドラはすべてasync関数です。戻り値の型を明示することで、その関数をimportして使用するクライアント・サーバー両方の箇所でIDEが引数と戻り値を提示します。明示しない場合、`as User`のような強制キャストがコードベース全体に積み重なっていきます。

async関数 = 常にPromiseを返す、await = 展開する

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

ts
async function getUser(): Promise<User> {
  return { id: 1, name: '山田太郎' };
  //       ↑ Userオブジェクトをそのままreturn
  //         実際の戻り値の型は自動的にPromise<User>
}
  • async が付いた関数は常にPromiseを返します
  • 戻り値の型は Promise<...> の形式でなければなりません(それ以外の型をシグネチャに書くとコンパイルエラーになります)。
  • 本体で return value すると、自動的に Promise.resolve(value) としてラップされます。
  • 本体で throw err すると、自動的に Promise.reject(err) になります。

2. awaitの結果は自動推論される

ts
async function loadAll() {
  const user: User = await getUser();          // 推論OK
  const orders: Order[] = await getOrders(user.id); // チェーン推論
  return { user, orders };
}
// loadAll の戻り値の型は自動的に Promise<{ user: User; orders: Order[] }>

3. catchのerrはunknown(TS 4.4+)

ts
try {
  await dangerousAction();
} catch (err) {
  // err の型は unknown — そのままでは .message にアクセス不可
  // err.message; ❌ Object is of type 'unknown'

  if (err instanceof Error) {
    console.log(err.message); // ✅ 絞り込まれる
  } else {
    console.log('不明なエラー:', err);
  }
}

4. Promiseを返す関数とasync関数は等価

ts
// 両関数のシグネチャは同じ
function a(): Promise<number> {
  return Promise.resolve(42);
}
async function b(): Promise<number> {
  return 42;
}
// a() と b() の呼び出し元は完全に同じように動作

好みで使い分けられますが、本体の中で await を使いたい場合は async が必須です。

💻 🅰️ JSスタイル — 戻り値の型が不透明、errもany
// ❌ JS — async関数のシグネチャが貧弱

async function fetchUser(id) {
  const res = await fetch(`/api/users/${id}`);
  if (!res.ok) throw new Error('失敗');
  return res.json(); // 戻り値はPromise<???>
}

async function main() {
  try {
    const user = await fetchUser(42);
    console.log(user.name);     // userが何であるかIDEが分からない
    console.log(user.naem);     // ❌ タイプミスだがコンパイル通過
  } catch (err) {
    console.log(err.message);   // errがErrorである保証はないがそのままアクセス
    // もし throw '文字列' されたものなら err.message は undefined
  }
}
main();
💻 🅱️ TSスタイル — シグネチャ + await推論 + errの絞り込み
// ✅ TS — シグネチャに戻り値の型を明示 + エラー安全処理

interface User {
  id: number;
  name: string;
  email: string;
}

class ApiError extends Error {
  constructor(message: string, public status: number) {
    super(message);
    this.name = 'ApiError';
  }
}

async function fetchUser(id: number): Promise<User> {
  const res = await fetch(`/api/users/${id}`);
  if (!res.ok) throw new ApiError('失敗', res.status);
  return res.json() as Promise<User>;
  // または zod などでランタイム検証後に返却
}

async function main(): Promise<void> {
  try {
    const user = await fetchUser(42);
    // user は User として推論される
    console.log(user.name);       // ✅ 自動補完
    // console.log(user.naem);    // ❌ コンパイル拒否
    console.log(user.email);      // ✅ User に存在する
  } catch (err) {
    // err: unknown — そのまま .message にアクセス不可
    if (err instanceof ApiError) {
      console.log(`API エラー ${err.status}: ${err.message}`);
    } else if (err instanceof Error) {
      console.log('一般エラー:', err.message);
    } else {
      console.log('不明なエラー:', err);
    }
  }
}
main();

💡 💡 async/await TypeScriptの要点5選

1. async関数の戻り値の型はシグネチャに明示する

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

シグネチャを見るだけで呼び出し元が意図を理解できます。

2. awaitの結果は推論に任せてもOK

ts
const user = await getUser(42); // user: User として自動推論

明示すると可読性が上がりますが、省略しても安全です。

3. errはunknown — 絞り込んで使う

ts
catch (err) {
  if (err instanceof Error) console.log(err.message);
}

tsconfiguseUnknownInCatchVariables: true はTS 4.4+ strictモードのデフォルト設定です。

4. async関数の中でPromiseを二重にラップしない

ts
// ❌ 二重ラップ
async function bad() { return Promise.resolve(42); }
// ✅ そのまま return
async function good() { return 42; }

5. Promise.allの結果はタプルで受け取る(promiseレッスン済み)

ts
const [user, orders] = await Promise.all([getUser(1), getOrders(1)]);
// user: User, orders: Order[]

⚡ 実際に試してみよう — async/await

型を取り除いた実行可能バージョンです。awaitによる順次処理とtry/catchによるエラーハンドリングを確認しましょう。
✏️ JS 코드
📟 コンソール出力
▶ 実行ボタンを押してください
⚠️ ブラウザのサンドボックスで実行 — console.log()のみ対応、alert/fetchは不可

確認クイズ

TS 4.4+において `try { ... } catch (err) { console.log(err.message); }` がコンパイルエラーになる理由は?
💡 TS 4.4から`useUnknownInCatchVariables`がstrictモードのデフォルトになり、`catch (err)`の`err`の型が`any`から`unknown`に変わりました。`unknown`には`.message`がないため、`if (err instanceof Error)`のような型の絞り込みを行う必要があります。より安全なデフォルト値として推奨されています。
async / await — 戻り値の型と err: unknown - TypeScript