mcpField
A
async-over-callbacks

async-over-callbacks

github.com/marius-townhouse開発642 回インストール38 週間前に更新
リポジトリ

async-over-callbacks について

TypeScript の非同期処理で、コールバックより async/await と Promise を優先するためのスキルです。処理の合成、エラー処理、コールバック API のラップ方法を扱います。

主な機能

  1. 01新規コードで async/await を優先
  2. 02逐次処理と並行処理の合成
  3. 03try/catch によるエラー処理
  4. 04コールバック API の Promise 化

活用シーン

  1. 01ネストしたコールバックのリファクタリング
  2. 02複数の非同期リクエストの調整
  3. 03非同期コードの失敗への対処

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/marius-townhouse/effective-typescript-skills --skill async-over-callbacks

インストール後、Claude Code で/async-over-callbacksを入力して読み込まれたか確認します。

詳細

コールバックではなく async 関数を使う

概要

コードをすっきりさせ、型の流れを改善するために、コールバックより async/await を優先してください。

コールバックを使うと、ネストが深くなり、追いにくいコードになります。Promise と async/await を使えば構造がフラットになり、型が自然に伝播し、エラー処理も改善できます。

このスキルを使う場面

  • 非同期コードを書くとき
  • コールバック形式の API を使おうとしているとき
  • 複数の非同期処理を連結するとき
  • 並行処理を組み合わせる必要があるとき
  • Promise を返す API を使うとき

絶対に守るルール

ALWAYS prefer async/await over callbacks for new code.

覚えておきましょう:

  • async/await は Promise の構文糖衣です
  • 型は Promise を通じて自動的に伝播します
  • try/catch を使うとエラー処理がすっきりします
  • 並行処理を簡単に組み合わせられます

検出:コールバックのピラミッド

ネストしたコールバック(「破滅のピラミッド」)を見つけたら、async/await にリファクタリングしてください。

// ❌ Callback hell - hard to read, types don't flow well
fetchURL(url1, function(response1) {
fetchURL(url2, function(response2) {
fetchURL(url3, function(response3) {
// ... deeply nested
console.log(1);
});
console.log(2);
});
console.log(3);
});
console.log(4);
// Logs: 4, 3, 2, 1 (confusing order!)

async/await による解決策

// ✅ Clean, flat, readable
async function fetchPages() {
const response1 = await fetch(url1);
const response2 = await fetch(url2);
const response3 = await fetch(url3);
// Execution order matches code order
}

型の伝播が改善される理由

コールバックでは型注釈を手動で記述する必要があります
// ❌ Callbacks - you must annotate types
function fetchUser(
  id: string,
  callback: (user: User | null, error?: Error) => void
) {
  // ...
}

fetchUser('123', (user, error) => {
  if (error) { /* handle */ }
  if (user) { /* use user */ }
});
Promise では型が自動的に引き継がれます
// ✅ Promises - types flow through
async function fetchUser(id: string): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  return response.json();  // TypeScript knows this returns Promise<User>
}

const user = await fetchUser('123');
//    ^? const user: User

非同期処理の組み合わせ

順次処理
async function getFullUserData(id: string) {
  const user = await fetchUser(id);
  const posts = await fetchPosts(user.id);
  const comments = await fetchComments(posts);
  return { user, posts, comments };
}
// Return type is automatically inferred
Promise.all による並行処理
async function fetchAllPages() {
  // Run all fetches concurrently, wait for all to complete
  const [page1, page2, page3] = await Promise.all([
    fetch(url1),
    fetch(url2),
    fetch(url3),
  ]);
  // Types are inferred: [Response, Response, Response]
}
Promise.race による競合処理
async function fetchWithTimeout(url: string, ms: number) {
  return Promise.race([
    fetch(url),
    new Promise<never>((_, reject) =>
      setTimeout(() => reject(new Error('Timeout')), ms)
    ),
  ]);
}

エラー処理

コールバック:エラーファースト規約(手動)
// ❌ Manual error handling, easy to forget
fetchData(url, (error, data) => {
  if (error) {
    console.error(error);
    return;
  }
  // use data
});
async/await:自然な try/catch
// ✅ Standard exception handling
async function fetchData(url: string) {
  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }
    return await response.json();
  } catch (error) {
    console.error('Fetch failed:', error);
    throw error;  // Re-throw or handle
  }
}

よく使われるパターン

async 関数からは必ず Promise を返す
// ❌ Inconsistent return type
function getQuote(ticker: string) {
  if (cache[ticker]) {
    return cache[ticker];  // Returns number
  }
  return fetch(`/quote?t=${ticker}`)
    .then(r => r.json());  // Returns Promise<number>
}
// Type: number | Promise<number> - confusing!

// ✅ Consistent Promise return
async function getQuote(ticker: string): Promise<number> {
  if (cache[ticker]) {
    return cache[ticker];  // Automatically wrapped in Promise
  }
  const response = await fetch(`/quote?t=${ticker}`);
  return response.json();
}
明確にするため、戻り値の型を注釈する
// ✅ Explicit return type catches mistakes
async function fetchUser(id: string): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  // TypeScript ensures we return a User
  return response.json();
}
部分的な失敗には Promise.allSettled を使う
async function fetchAllUsers(ids: string[]) {
  const results = await Promise.allSettled(
    ids.map(id => fetchUser(id))
  );
  
  // Handle both successes and failures
  const users: User[] = [];
  for (const result of results) {
    if (result.status === 'fulfilled') {
      users.push(result.value);
    } else {
      console.error('Failed:', result.reason);
    }
  }
  return users;
}

コールバックを Promise に変換する

util.promisify を使う(Node.js)
import { promisify } from 'util';
import { readFile } from 'fs';

const readFileAsync = promisify(readFile);

async function loadConfig() {
  const data = await readFileAsync('config.json', 'utf-8');
  return JSON.parse(data);
}
手動で Promise 化する
function fetchURLAsync(url: string): Promise<string> {
  return new Promise((resolve, reject) => {
    fetchURL(url, (response, error) => {
      if (error) {
        reject(error);
      } else {
        resolve(response);
      }
    });
  });
}

async 関数内での即時実行

// async functions return a Promise, even with immediate values
async function getValue(): Promise<number> {
  return 42;  // Wrapped in Promise.resolve(42)
}

// To unwrap, you must await
const value = await getValue();
//    ^? const value: number

プレッシャーに対する対応策

1. 「コールバックのほうが速い」

プレッシャー:「Promise にはオーバーヘッドがあり、コールバックのほうが高パフォーマンスです」

回答: オーバーヘッドは無視できるほど小さいです。コードの明瞭さと型安全性のほうが重要です。

対応: async/await を使ってください。パフォーマンスに問題があると思われる場合は、プロファイリングしてください。

2. 「API がコールバックにしか対応していない」

プレッシャー:「このライブラリはコールバックを使っているので、こちらも使う必要があります」

回答: コールバック API を Promise でラップしてください。

対応: promisify を使うか、Promise ラッパーを作成してください。

3. 「すでにコード全体でコールバックを使っている」

プレッシャー:「既存コードとの一貫性が必要です」

回答: 段階的に移行してください。新しいコードでは async/await を使うべきです。

対応: 古い API をラップし、新しいコードは async/await で書いてください。

注意すべき兆候 — いったん立ち止まって再検討してください

  • ネストしたコールバック(破滅のピラミッド)
  • 値を返すこともあれば、Promise を返すこともある関数
  • エラーファーストのコールバックによる手動のエラー処理
  • async/await と .then() チェーンを不必要に混在させている

よくある誤った正当化

言い訳実際のところ
「コールバックのほうがシンプル」async/await のほうが読みやすく、保守しやすい
「Promise のオーバーヘッドが大きすぎる」実際には無視できるほど小さく、明瞭さのほうが重要
「チームはコールバックに慣れている」async/await は現代の JavaScript で標準です

クイックリファレンス

パターンコールバックasync/await
順次処理ネストしたコールバック順次 await
並行処理手動で追跡Promise.all
エラー処理エラーファースト規約try/catch
型推論手動の型注釈自動的に伝播
組み合わせ難しい自然に行える

要点

async/await を使うと、型推論が改善され、コードがすっきりします。

コールバックを使うとコードが深くネストし、型もうまく伝播しません。Promise と async/await を使えば構造がフラットになり、自然に組み合わせられ、標準的な try/catch でエラーを処理でき、TypeScript が型を自動的に推論できます。新しい非同期コードにはすべて async/await を使ってください。

参考資料

Dan Vanderkam 著「Effective TypeScript」項目 27「型の流れを改善するため、コールバックではなく async 関数を使う」に基づいています。

コメント