TypeScript 異步與 Promise 型別:深入探討 Promise.all / Promise.race 的型別運算
簡介
在現代前端開發中,非同步流程已成為日常。無論是呼叫 REST API、讀取檔案或是與 Web Worker 溝通,都離不開 Promise。TypeScript 為 Promise 提供了靜態型別,讓開發者在編譯階段就能捕捉到錯誤,提升程式碼的安全性與可讀性。
Promise.all 與 Promise.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.all,race 不會保留每個位置的型別,只會取 所有元素的聯合型別(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.all 與 AbortController
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 const 或 tuple,或直接把變數宣告為 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,在需要時中止請求。 |
最佳實踐
- 盡量使用 tuple(
as const)保留每個 Promise 的精確型別。 - 在
Promise.race後加入型別保護,避免union帶來的安全隱憂。 - 若需要同時取得成功與失敗,選擇
Promise.allSettled而非Promise.all。 - 利用
Awaited<T>讓自訂工具函式的型別更易讀。 - 結合
AbortController,在併發請求過多時提供取消機制。
實際應用場景
批次載入多筆資源
- 例如同時取得使用者資訊、權限設定與 UI 主題。使用
Promise.all搭配 tuple,一次取得全部資料,並在 UI 中一次渲染。
- 例如同時取得使用者資訊、權限設定與 UI 主題。使用
競賽式請求(Race)
- 在多個備援 API 中,只要任一個回應最快即返回結果,常見於 CDN 或第三方服務的容錯機制。
全局超時控制
- 使用
Promise.race([task, timeout])來為單一請求添加超時邏輯,或使用AbortController與Promise.all同時管理多個請求的生命週期。
- 使用
錯誤收集與報表
Promise.allSettled可在一次批次作業結束後,彙總成功與失敗的項目,用於後端日誌或前端錯誤提示。
型別安全的資料轉換
- 取得多筆資料後,利用
await直接解構 tuple,搭配 型別推斷,在後續的資料處理階段避免手動轉型的錯誤。
- 取得多筆資料後,利用
總結
Promise.all 與 Promise.race 是 TypeScript 中處理非同步集合的核心工具。透過 tuple、as const、Awaited<T> 等語法,我們可以在編譯期就確保每個 Promise 的回傳型別被正確保留,進而在 await 後獲得 精確且安全 的資料結構。
Promise.all:保留每個位置的型別(使用 tuple),適合需要全部結果的情境。Promise.race:回傳所有可能型別的聯合,需要自行做 type guard。Promise.allSettled:同時取得成功與失敗資訊,型別仍保持 tuple。
掌握這些型別運算的細節,能讓你在開發大型前端應用或 Node.js 服務時,減少因型別不確定而產生的執行時錯誤,提升程式碼的可維護性與開發效率。
快把這些技巧寫進你的程式碼基礎,讓 TypeScript 成為你非同步程式的最佳防護盾吧!