本文 AI 產出,尚未審核

TypeScript 異步與 Promise 型別:深入探討 Promise.all / Promise.race 的型別運算


簡介

在現代前端開發中,非同步流程已成為日常。無論是呼叫 REST API、讀取檔案或是與 Web Worker 溝通,都離不開 Promise。TypeScript 為 Promise 提供了靜態型別,讓開發者在編譯階段就能捕捉到錯誤,提升程式碼的安全性與可讀性。

Promise.allPromise.race 是兩個常用的集合工具:前者等待所有 Promise 完成,後者只要其中一個先完成就返回結果。雖然使用上很簡單,但在型別推論上卻容易產生 「型別寬度」「遺失資訊」 的問題。本文將逐步說明這兩個 API 在 TypeScript 中的型別運算機制,並提供實務範例、常見陷阱與最佳實踐,幫助你在寫非同步程式時既 安全高效


核心概念

1. Promise.all 的基本型別

Promise.all<T>(promises: readonly (PromiseLike<T> | T)[]) 會回傳 Promise<T[]>
簡單來說,傳入的每一個元素的型別 T 會被 「串聯」 成一個陣列。

// 範例 1:所有 Promise 的回傳型別相同
const p1 = Promise.resolve(10);
const p2 = Promise.resolve(20);
const p3 = Promise.resolve(30);

const all = Promise.all([p1, p2, p3]); // Promise<number[]>

重點:若所有 Promise 回傳相同型別,Promise.all 會推導為 Promise<T[]>,其中 T 為共同型別(此例為 number)。

2. 異質陣列:使用 Tuple 取得精確型別

當陣列裡的 Promise 型別不一致 時,TypeScript 會利用 tuple(固定長度且各元素型別可不同)保留每個位置的資訊。

// 範例 2:異質 Promise 陣列
const pStr = Promise.resolve('hello');
const pNum = Promise.resolve(42);
const pBool = Promise.resolve(true);

const mixed = Promise.all([pStr, pNum, pBool]);
// 推導結果:Promise<[string, number, boolean]>

此時 mixed 的型別是 Promise<[string, number, boolean]>,而不是 Promise<Array<string | number | boolean>>,因此在解構時可以得到 精確的型別

async function demo() {
  const [msg, count, flag] = await mixed;
  // msg: string, count: number, flag: boolean
}

技巧:若你使用 as const,可以強制 TypeScript 把普通陣列視為 tuple,避免因為「寬鬆陣列」而失去型別資訊。

const tuple = [pStr, pNum, pBool] as const;
const allTuple = Promise.all(tuple); // Promise<[Promise<string>, Promise<number>, Promise<boolean>]>

3. Promise.allSettled 與型別保留

Promise.allSettled 會把每個 Promise 包裝成 { status: "fulfilled" | "rejected"; value?: T; reason?: any },但它同樣會保留 tuple 型別:

const allSettled = Promise.allSettled([pStr, pNum]);
// 推導結果:Promise<[{ status: "fulfilled"; value: string }, { status: "fulfilled"; value: number }]>

這在需要同時取得成功與失敗結果時非常有用,且不會因為 any 失去型別安全。

4. Promise.race 的型別推論

Promise.race<T>(promises: readonly (PromiseLike<T> | T)[]) 回傳 Promise<T>
不同於 Promise.allrace 不會保留每個位置的型別,只會取 所有元素的聯合型別(union):

// 範例 3:race 的聯合型別
const race = Promise.race([pStr, pNum, pBool]);
// 推導結果:Promise<string | number | boolean>

因此,當你使用 await race 時,得到的結果只能是 string | number | boolean,若要進一步分辨,需要自行 type guard

async function raceDemo() {
  const result = await race;
  if (typeof result === 'string') {
    console.log('文字:', result);
  } else if (typeof result === 'number') {
    console.log('數字:', result);
  } else {
    console.log('布林:', result);
  }
}

5. 用 Awaited<T> 取得最終解析型別

從 TypeScript 4.5 起,引入了內建的 Awaited<T>,可用來取得 Promise 內部的最終型別,對於自訂函式的回傳型別非常有幫助:

type Resolved<T> = Awaited<T>;

type A = Resolved<Promise<string>>; // string
type B = Resolved<string>;          // string

在實作 Promise.all 的類型工具時,可以結合 Awaited 讓型別更精確:

type All<T extends readonly unknown[]> = Promise<{
  [K in keyof T]: Awaited<T[K]>;
}>;

// 使用
type Result = All<[Promise<string>, number, Promise<boolean>]>;
// Result = Promise<[string, number, boolean]>

程式碼範例(實用 5 範例)

範例 A:基本 Promise.all(相同型別)

// 同步取得三筆使用者 ID
const ids = [1, 2, 3];
const fetchUser = (id: number) => fetch(`/api/user/${id}`).then(r => r.json());

async function getAllUsers() {
  const users = await Promise.all(ids.map(fetchUser));
  // users: any[](若未提供泛型,會退化為 any)
  console.log(users);
}

建議:為 fetchUser 加上回傳型別 Promise<User>,讓 Promise.all 推導為 Promise<User[]>

範例 B:異質 Promise.all + as const

type User = { id: number; name: string };
type Config = { theme: string };

const getUser = (id: number): Promise<User> => fetch(`/api/user/${id}`).then(r => r.json());
const getConfig = (): Promise<Config> => fetch('/api/config').then(r => r.json());

const promises = [getUser(1), getConfig()] as const; // 變成 tuple
const all = Promise.all(promises);
// all: Promise<[User, Config]>

async function demo() {
  const [user, config] = await all;
  console.log(user.name, config.theme);
}

範例 C:Promise.race 與型別保護

const fast = new Promise<string>((res) => setTimeout(() => res('快'), 100));
const slow = new Promise<number>((res) => setTimeout(() => res(999), 300));

const winner = Promise.race([fast, slow]); // Promise<string | number>

async function raceDemo() {
  const result = await winner;
  if (typeof result === 'string') {
    console.log('先到的是字串:', result);
  } else {
    console.log('先到的是數字:', result);
  }
}

範例 D:自訂 allSettled 型別工具

type AllSettled<T extends readonly unknown[]> = Promise<{
  [K in keyof T]: T[K] extends Promise<infer R>
    ? { status: 'fulfilled'; value: R }
    : { status: 'rejected'; reason: unknown };
}>;

const pA = Promise.resolve('A');
const pB = Promise.reject(new Error('B'));

const settled = Promise.allSettled([pA, pB]) as AllSettled<[Promise<string>, Promise<number>]>;

settled.then((results) => {
  // results: [{status:'fulfilled',value:string}, {status:'rejected',reason:unknown}]
});

範例 E:實務中結合 Promise.allAbortController

async function fetchWithTimeout<T>(url: string, ms: number): Promise<T> {
  const controller = new AbortController();
  const id = setTimeout(() => controller.abort(), ms);

  const resp = await fetch(url, { signal: controller.signal });
  clearTimeout(id);
  return resp.json() as Promise<T>;
}

// 同時取得多筆資料,若任一超時即拋出錯誤
async function loadAll() {
  const urls = ['/api/a', '/api/b', '/api/c'] as const;
  const tasks = urls.map((u) => fetchWithTimeout<any>(u, 2000));
  const data = await Promise.all(tasks); // Promise<[any, any, any]>
  console.log(data);
}

常見陷阱與最佳實踐

陷阱 說明 解決方式
陣列被寬化為 any[] Promise.all 的參數是普通陣列,TypeScript 可能將其視為 Promise<any[]>,失去型別資訊。 使用 as consttuple,或直接把變數宣告為 readonly [...]
Promise.race 的聯合型別過於寬鬆 取得結果後若未做 type guard,會出現 `string number` 等不易操作的型別。
忘記處理 rejected Promise Promise.all 只要有一個失敗就會直接 reject,導致其他成功結果被忽略。 使用 Promise.allSettled 或在每個 Promise 前加上 .catch(() => undefined),再自行過濾結果。
過度使用 any 為了省事直接把回傳型別寫成 any,會失去 TypeScript 的好處。 為每個 async 函式明確定義回傳型別,或使用 Awaited<T> 推導。
忽略 AbortController 大量併發請求時,若使用者離開頁面仍在背景執行,會浪費資源。 Promise.all 前建立 AbortController,在需要時中止請求。

最佳實踐

  1. 盡量使用 tupleas const)保留每個 Promise 的精確型別。
  2. Promise.race 後加入型別保護,避免 union 帶來的安全隱憂。
  3. 若需要同時取得成功與失敗,選擇 Promise.allSettled 而非 Promise.all
  4. 利用 Awaited<T> 讓自訂工具函式的型別更易讀。
  5. 結合 AbortController,在併發請求過多時提供取消機制。

實際應用場景

  1. 批次載入多筆資源

    • 例如同時取得使用者資訊、權限設定與 UI 主題。使用 Promise.all 搭配 tuple,一次取得全部資料,並在 UI 中一次渲染。
  2. 競賽式請求(Race)

    • 在多個備援 API 中,只要任一個回應最快即返回結果,常見於 CDN 或第三方服務的容錯機制。
  3. 全局超時控制

    • 使用 Promise.race([task, timeout]) 來為單一請求添加超時邏輯,或使用 AbortControllerPromise.all 同時管理多個請求的生命週期。
  4. 錯誤收集與報表

    • Promise.allSettled 可在一次批次作業結束後,彙總成功與失敗的項目,用於後端日誌或前端錯誤提示。
  5. 型別安全的資料轉換

    • 取得多筆資料後,利用 await 直接解構 tuple,搭配 型別推斷,在後續的資料處理階段避免手動轉型的錯誤。

總結

Promise.allPromise.race 是 TypeScript 中處理非同步集合的核心工具。透過 tupleas constAwaited<T> 等語法,我們可以在編譯期就確保每個 Promise 的回傳型別被正確保留,進而在 await 後獲得 精確且安全 的資料結構。

  • Promise.all:保留每個位置的型別(使用 tuple),適合需要全部結果的情境。
  • Promise.race:回傳所有可能型別的聯合,需要自行做 type guard。
  • Promise.allSettled:同時取得成功與失敗資訊,型別仍保持 tuple。

掌握這些型別運算的細節,能讓你在開發大型前端應用或 Node.js 服務時,減少因型別不確定而產生的執行時錯誤,提升程式碼的可維護性與開發效率。

快把這些技巧寫進你的程式碼基礎,讓 TypeScript 成為你非同步程式的最佳防護盾吧!