TypeScript – 異步與 Promise 型別(Async & Promise)
主題:泛型 Promise
簡介
在前端與 Node.js 開發中,非同步流程是日常必備的技術。Promise 為 ES6 引入的標準介面,讓我們可以以「成功」或「失敗」的狀態,清晰地描述非同步操作。
TypeScript 在 Promise 上加入了 泛型(generic)支援,使開發者能在編譯階段就捕捉到回傳資料的型別錯誤,提升程式的安全性與可維護性。
本篇文章將從概念說明、實作範例、常見陷阱到最佳實踐,完整介紹 泛型 Promise 的使用方式,幫助初學者快速上手,同時提供中級開發者進階的技巧。
核心概念
1. 為什麼要使用泛型 Promise?
Promise<T> 中的 T 代表 Promise 最終 resolve 時所返回的資料型別。
function getUser(): Promise<User> { ... }
如果沒有寫泛型,Promise 會被視為 Promise<any>,編譯器無法幫助我們檢查 User 型別,容易產生隱蔽的錯誤。
重點:使用泛型可以在 編譯階段 捕捉型別不匹配,降低執行時的錯誤機率。
2. 基本語法
// 宣告一個回傳字串的 Promise
function fetchMessage(): Promise<string> {
return new Promise<string>((resolve, reject) => {
setTimeout(() => resolve('Hello, TypeScript!'), 1000);
});
}
new Promise<T>(executor):executor內的resolve參數會自動被推斷為T。async函式預設回傳Promise<ReturnType>,如果明確寫出型別,會更具可讀性:
async function getNumber(): Promise<number> {
return 42; // 編譯器會自動包成 Promise<number>
}
3. 多層 Promise 與型別推斷
當 Promise 內再回傳 Promise 時,TypeScript 會 自動展開(flatten)最內層的型別:
function outer(): Promise<Promise<number>> {
return Promise.resolve(Promise.resolve(10));
}
// 呼叫時仍得到 number
async function demo() {
const n = await outer(); // n 被推斷為 number
}
這是因為 await 以及 .then 會把 Promise<Promise<T>> 轉成 Promise<T>。
4. Promise.all 與泛型
Promise.all 接收一個 可迭代的 Promise 陣列,返回一個 泛型 Tuple,每個元素的型別對應原始陣列的順序:
const p1 = fetchMessage(); // Promise<string>
const p2 = getNumber(); // Promise<number>
const p3 = fetchUser(); // Promise<User>
async function combine() {
const [msg, num, user] = await Promise.all([p1, p2, p3]);
// msg: string, num: number, user: User
}
如果使用 Promise.allSettled,則回傳的型別會是 PromiseSettledResult<T>[],需要自行判斷 status。
5. 自訂泛型 Promise 型別
在大型專案中,我們常會封裝一套 統一的回傳格式(例如 { code: number; data: T; msg: string }),此時可以自行定義一個泛型介面,並搭配 Promise:
interface ApiResult<T> {
code: number;
data: T;
msg: string;
}
// 例:取得商品列表
function fetchProducts(): Promise<ApiResult<Product[]>> {
return fetch('/api/products')
.then(res => res.json())
.then(json => ({
code: json.code,
data: json.data as Product[],
msg: json.msg,
}));
}
這樣呼叫端只要寫:
async function showProducts() {
const result = await fetchProducts();
if (result.code === 0) {
result.data.forEach(p => console.log(p.name));
}
}
程式碼範例
以下提供 5 個實用範例,說明如何在不同情境下使用泛型 Promise。
範例 1:簡易的非同步計算
// 計算兩個數字的乘積,回傳 Promise<number>
function multiplyAsync(a: number, b: number): Promise<number> {
return new Promise<number>((resolve) => {
setTimeout(() => resolve(a * b), 500);
});
}
// 使用 async/await
async function demoMultiply() {
const result = await multiplyAsync(3, 7);
console.log('乘積 =', result); // 乘積 = 21
}
demoMultiply();
說明:
Promise<number>讓result的型別在編譯期即被確定為number,不會因為any而失去 IntelliSense。
範例 2:從 API 取得 JSON 並映射型別
interface Post {
id: number;
title: string;
body: string;
}
// 取得單一文章,回傳 Promise<Post>
function fetchPost(id: number): Promise<Post> {
return fetch(`https://jsonplaceholder.typicode.com/posts/${id}`)
.then(res => res.json() as Promise<Post>);
}
// 呼叫
async function showPost() {
const post = await fetchPost(1);
console.log(`#${post.id} ${post.title}`);
}
showPost();
重點:
as Promise<Post>告訴 TypeScript 這段 JSON 會符合Post介面,避免any。
範例 3:自訂 API 回傳格式(泛型介面)
interface ApiResponse<T> {
status: 'ok' | 'error';
payload: T;
}
// 取得使用者資料,回傳 Promise<ApiResponse<User>>
function getUser(id: number): Promise<ApiResponse<User>> {
return fetch(`/api/users/${id}`)
.then(r => r.json())
.then(data => ({
status: data.status,
payload: data.user as User,
}));
}
// 使用
async function greetUser() {
const resp = await getUser(5);
if (resp.status === 'ok') {
console.log(`嗨,${resp.payload.name}`);
}
}
說明:透過
ApiResponse<T>,不同 API 只要改變T即可重用同一套型別定義。
範例 4:Promise.all 搭配不同型別
function delay<T>(value: T, ms: number): Promise<T> {
return new Promise(resolve => setTimeout(() => resolve(value), ms));
}
async function parallelDemo() {
const [msg, num, flag] = await Promise.all([
delay('完成', 800), // Promise<string>
delay(123, 400), // Promise<number>
delay(true, 600), // Promise<boolean>
]);
// TypeScript 已正確推斷型別
console.log(msg, num, flag); // 完成 123 true
}
parallelDemo();
技巧:
Promise.all會自動產生 Tuple 型別,讓每個變數的型別皆被保留。
範例 5:錯誤處理與 Promise.reject
function fetchWithError(): Promise<string> {
return new Promise<string>((_, reject) => {
setTimeout(() => reject(new Error('網路錯誤')), 300);
});
}
async function errorDemo() {
try {
const data = await fetchWithError();
console.log(data);
} catch (err) {
// err 被推斷為 unknown,需自行斷言
if (err instanceof Error) {
console.error('捕獲錯誤:', err.message);
}
}
}
errorDemo();
提醒:即使 Promise 宣告了泛型,
reject的型別仍是unknown,所以在catch中要做好型別斷言或使用instanceof。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
| 忘記寫泛型 | Promise<any> 失去型別安全。 |
永遠為 Promise 加上 <T>,即使是 Promise<void>。 |
resolve 傳入錯誤型別 |
resolve(123 as unknown as string) 會在編譯期通過,但執行時不符合預期。 |
讓編譯器自行推斷,或使用 as 前先確認型別。 |
await 直接套用在非 Promise |
await 5 會被包成 Promise<number>,但語意不清。 |
僅在確定回傳 Promise 時使用 await。 |
Promise.all 中的 null 或 undefined |
這兩者不會被視為 Promise,會直接作為結果返回,導致 Tuple 型別不一致。 | 確保陣列內每個元素皆為 Promise<T>,或使用 Promise.allSettled。 |
錯誤類型為 unknown |
catch 區塊內的錯誤預設為 unknown,若直接存取 .message 會報錯。 |
使用 if (err instanceof Error) 進行型別守衛,或自行定義錯誤介面。 |
最佳實踐
- 明確標註返回型別:
function foo(): Promise<Result>,即使編譯器能推斷。 - 使用
async/await搭配try/catch,可讓錯誤流更清晰。 - 在大型專案中抽象 API 回傳:建立
ApiResponse<T>、PagedResult<T>等通用介面,減少重複程式碼。 - 避免在
Promise內部做過多邏輯:保持executor簡潔,只負責「非同步」與「resolve/reject」的交接。 - 利用 TypeScript 的型別推斷:在
then、catch、finally中不必重複寫型別,讓編譯器自行傳遞。
實際應用場景
前端資料抓取
- 使用
fetch取得 JSON,配合Promise<ApiResponse<T>>,讓 UI 元件在取得資料前即可得到正確的型別提示,減少渲染錯誤。
- 使用
Node.js 後端服務
- 讀寫資料庫(例如
mongodb、sequelize)時,回傳Promise<Model>,讓服務層可以直接使用模型屬性,避免手動any轉型。
- 讀寫資料庫(例如
第三方 SDK 包裝
- 把原生 callback API 包裝成
Promise<T>,例如fs.readFile→readFileAsync(path): Promise<Buffer>,讓使用者可以使用await。
- 把原生 callback API 包裝成
批次處理與併發
- 使用
Promise.all同時發送多筆請求(如同時取得多個使用者資料),配合泛型 Tuple,讓每筆結果的型別都被保留。
- 使用
錯誤統一管理
- 建立
Result<T, E>之類的泛型型別,讓成功與失敗的資訊都能在同一個 Promise 中回傳,提升錯誤處理的一致性。
- 建立
總結
- 泛型 Promise 為 TypeScript 提供了在非同步程式碼中「編譯期即驗證」的能力,讓開發者可以在 IDE 中即時得到型別提示與錯誤警告。
- 正確使用
Promise<T>、async/await、Promise.all、自訂回傳介面,能大幅提升程式的可讀性、可維護性與安全性。 - 避免常見陷阱(忘寫泛型、錯誤型別、混用
null/undefined),遵循最佳實踐,即可在實務專案中發揮 泛型 Promise 的最大威力。
掌握了這些概念與技巧,你就能在任何 TypeScript 專案中,以型別安全的方式處理非同步流程,寫出更穩定、易除錯的程式碼。祝開發順利!