C
TypeScript/非同期/Lesson 01

同期 vs 非同期 — TypeScript はコールバックのシグネチャまで保護する

25分·theory
このチャプター
1/7
TypeScript

同期 vs 非同期 — TypeScript はコールバックのシグネチャまで保護する

💡 なぜ学ぶべきか? — 同期・非同期は同じでも、安全網が違う

🎯 同期・非同期のランタイム動作は JS と TS で同じです — 違いは「コールバックや戻り値の型を誰が保証するか」という点にあります。
💼 JS では `setTimeout(cb, 1000)` の `cb` がどんな引数を受け取るか分かりません。TS ではコールバックシグネチャを型として固定できます。
非同期関数が何を返すか (`Promise` なのか `Promise` なのか) をシグネチャに明記しておくと、呼び出し側コードで自動補完が効くようになります。
🔗 Node.js とブラウザでは `setTimeout` の戻り値の型が異なります (`NodeJS.Timeout` vs `number`) — TS は環境ごとに正確に推論します。
🏢 실무에서는
イベントハンドラ・タイマー・API 呼び出しはすべてコールバックを受け取ります。JS ではコールバック内で `e.target.value` と書くつもりが `e.target.valeu` とタイプミスしても、ランタイムになって初めて発覚します。TS では `(e: React.ChangeEvent) => void` のように型を付けることで、IDE の自動補完・タイプミス防止・構造変更の自動追跡をすべて提供します。

同期・非同期の動作は同じでも、コールバックの型が違う

1. ランタイムの動作は同一

ts
// 同期 — 上から下へ即時実行
console.log('1');
console.log('2');
console.log('3');

// 非同期 — setTimeout のコールバックはTask Queueへ
console.log('1');
setTimeout(() => console.log('2'), 0);
console.log('3');
// 出力: 1 → 3 → 2

TS でも JS でも、この動作は同じです。TypeScript はコンパイル後に型を取り除いて JavaScript として実行されるからです。

2. 違いは「コールバック・戻り値の型」の明示

ts
// JS
function onChange(e) {
  console.log(e.target.value); // e は any — タイプミスを指摘しない
}

// TS
function onChange(e: React.ChangeEvent<HTMLInputElement>) {
  console.log(e.target.value); // ✅ 自動補完、タイプミス時に赤線
}

3. 非同期関数の戻り値の型を固定する

ts
// JS — 呼び出し元はfetchUserが何を返すかシグネチャだけでは分からない
function fetchUser(id) {
  return fetch(`/api/${id}`).then(r => r.json());
}

// TS — シグネチャを見るだけで「Promise<User>が来る」と明確
async function fetchUser(id: number): Promise<User> {
  const res = await fetch(`/api/${id}`);
  return res.json();
}

4. setTimeout の戻り値は環境によって異なる — 1つのコードは1つの環境でしか安全でない

ts
// ❌ ブラウザ専用 — Node.jsではNodeJS.Timeoutオブジェクトが返され型エラー
const id1: number = setTimeout(() => {}, 1000);

// ❌ Node.js専用 — ブラウザ環境 (@types/nodeなし) ではNodeJS名前空間自体がなくコンパイルエラー
const id2: NodeJS.Timeout = setTimeout(() => {}, 1000);

// ✅ 両方の環境で安全 — TSがlib/@types/node設定に従って自動推論
const id3: ReturnType<typeof setTimeout> = setTimeout(() => {}, 1000);
clearTimeout(id3);

> 💡 ライブラリや汎用モジュールを作る際は必ず ReturnType<typeof setTimeout> を使いましょう。環境が確定しているアプリコードでのみ numberNodeJS.Timeout を直接書いても構いません。

💻 🅰️ JS の書き方 — コールバック引数の型が不明
// ❌ JS — コールバック・戻り値の形をシグネチャで表現できない

function fetchUser(id, callback) {
  setTimeout(() => {
    callback({ id, name: '山田太郎' }); // このコールバックが何を受け取るか呼び出し元が分からない
  }, 500);
}

fetchUser(42, (user) => {
  console.log(user.name);    // ✅ 運が良ければ動作
  console.log(user.naem);    // ❌ タイプミス — ランタイムでundefined、発見が遅れる
  console.log(user.email);   // ❌ 存在しないフィールド — ランタイムでundefined
});

// setTimeoutの戻り値も型なし
const timerId = setTimeout(() => console.log('tick'), 1000);
clearTimeout(timerId); // ブラウザならnumber、Nodeならオブジェクト — どちらもただの「値」として扱われる

// 非同期関数のシグネチャも貧弱
async function loadOrders(userId) {
  // 戻り値の型がPromise<何>なのか関数本体をすべて読まないと分からない
  const res = await fetch(`/api/orders?u=${userId}`);
  return res.json();
}
💻 🅱️ TS の書き方 — コールバックのシグネチャと戻り値の型を固定する
// ✅ TS — コールバック・戻り値の形を型で固定する

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

// コールバックシグネチャを明示 — (user: User) => void
function fetchUser(id: number, callback: (user: User) => void): void {
  setTimeout(() => {
    callback({ id, name: '山田太郎' });
  }, 500);
}

fetchUser(42, (user) => {
  // userの型がUserとして推論される
  console.log(user.name);  // ✅ 自動補完
  // console.log(user.naem); // ❌ コンパイル拒否 — 'naem'はUserにない
  // console.log(user.email); // ❌ コンパイル拒否 — Userにemailがない
});

// タイマーID — 環境に関わらず安全な推論
const timerId: ReturnType<typeof setTimeout> = setTimeout(
  () => console.log('tick'),
  1000,
);
clearTimeout(timerId);

// 非同期関数シグネチャに戻り値の型を明示
async function loadOrders(userId: number): Promise<Order[]> {
  const res = await fetch(`/api/orders?u=${userId}`);
  return res.json() as Promise<Order[]>;
}

interface Order { id: number; item: string; }

// 呼び出し元 — 戻り値がOrder[]であることをIDEが知っている
loadOrders(42).then((orders) => {
  orders.forEach((o) => console.log(o.item)); // ✅ 自動補完
});

💡 💡 JS と TS の核心的な違い(同期/非同期)

1. コールバックを受け取る関数は、コールバックのシグネチャを引数の型に埋め込む

ts
function onLoad(cb: (data: User[]) => void): void { ... }

2. 非同期関数はシグネチャに戻り値の型を明示する

ts
async function getUser(id: number): Promise<User> { ... }
// シグネチャを見るだけで「Promise<User> が返る」とわかる

3. タイマー ID は ReturnType<typeof setTimeout> で受け取る
ブラウザは number、Node は NodeJS.Timeout を返しますが、これで環境に関係なく安全です。

4. 非同期の結果をそのまま any に流さない
res.json() のデフォルト戻り値は Promise<any> です。キャストするか zod などのバリデーションで絞り込むか、境界で型を固定することが重要です。

5. コールバック地獄の答えは JS・TS どちらも同じ:async/await
TypeScript は async/await の型推論をより強力にサポートします。これは Promise のレッスンで扱った内容です。

⚡ 実際に試してみよう — 同期 vs 非同期

上記の 🅱️ TS コードから型を取り除いた実行可能バージョンです。動作自体は JS と TS で同一であることを確認します。
✏️ JS 코드
📟 コンソール出力
▶ 実行ボタンを押してください
⚠️ ブラウザのサンドボックスで実行 — console.log()のみ対応、alert/fetchは不可

確認クイズ

TypeScript でブラウザと Node.js の両方の環境で安全にタイマー ID を受け取るには、どうすれば良いですか?
💡 ブラウザでは `setTimeout` が `number` を返しますが、Node.js では `NodeJS.Timeout` オブジェクトを返します。`ReturnType<typeof setTimeout>` で受け取ることで、コンパイル環境 (lib 設定や `@types/node` の有無) に応じて正確に型が推論され、同じコードが両環境で安全にコンパイルされます。
同期 vs 非同期 — コールバックシグネチャまで保護 - TypeScript