本文 AI 產出,尚未審核
TypeScript
單元:異步與 Promise 型別(Async & Promise)
主題:Promise 型別(Promise<T>)
簡介
在現代前端與 Node.js 開發中,非同步流程已經是不可或缺的一環。無論是呼叫遠端 API、讀寫檔案,還是處理計時器,都會涉及到 Promise。
TypeScript 在 JavaScript 的 Promise 基礎上,加入了 泛型 (<T>) 讓開發者可以在編譯階段即掌握非同步結果的型別,從而大幅降低執行時錯誤的機會。
本單元將深入探討 Promise<T> 的概念、常見寫法與實務應用,並提供實用範例、陷阱與最佳實踐,幫助初學者快速上手,同時讓已有基礎的開發者更進一步提升型別安全與程式碼可讀性。
核心概念
1. Promise<T> 是什麼?
Promise<T> 表示 一個最終會解決 (resolve) 為型別 T 的非同步操作,或是被拒絕 (reject) 為 any(預設)。
// Promise<number> 最終會得到一個 number
declare const fetchUserAge: () => Promise<number>;
T:成功時的回傳值型別。reject:若未指定,預設為any,因此在 TypeScript 4.4+ 建議使用Promise<T, E>(自訂錯誤型別)或透過unknown來加強安全性。
2. Promise 的三種狀態
| 狀態 | 說明 |
|---|---|
| pending | 初始狀態,尚未決定成功或失敗 |
| fulfilled | 呼叫 resolve(value),value 必須符合 T |
| rejected | 呼叫 reject(reason),reason 為錯誤資訊 |
3. 基本語法與型別推斷
function delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
// 使用時,TypeScript 會自動推斷 Promise<void>
delay(1000).then(() => console.log('1 秒過去'));
重點:若不寫泛型,TS 會根據
resolve的參數自行推斷T。為了明確與可讀,建議 顯式寫出Promise<T>。
4. 實作範例:從 API 取得使用者資料
interface User {
id: number;
name: string;
email: string;
}
/**
* 取得使用者資訊,回傳 Promise<User>
*/
function fetchUser(id: number): Promise<User> {
return fetch(`https://api.example.com/users/${id}`)
.then((res) => {
if (!res.ok) throw new Error('Network response was not ok');
return res.json() as Promise<User>; // 型別斷言
});
}
// 呼叫方式
fetchUser(1)
.then((user) => console.log(user.name))
.catch((err) => console.error(err));
註解:
as Promise<User>告訴編譯器res.json()會回傳符合User結構的物件。若想更安全,可使用zod、io-ts等 runtime validation。
5. async / await 與 Promise<T> 的結合
async 函式會自動把回傳值包裝成 Promise<T>,其中 T 為 函式回傳值的型別。
/**
* 以 async/await 實作同樣的 fetchUser
*/
async function fetchUserAsync(id: number): Promise<User> {
const response = await fetch(`https://api.example.com/users/${id}`);
if (!response.ok) throw new Error('Network error');
const data: User = await response.json(); // 型別推斷
return data; // 隱含回傳 Promise<User>
}
// 使用
(async () => {
try {
const user = await fetchUserAsync(2);
console.log(user.email);
} catch (e) {
console.error(e);
}
})();
小技巧:在
async函式內 直接使用型別註記(如const data: User),可以在編譯階段捕捉 JSON 結構不符的錯誤。
6. 多個 Promise 的型別組合
6.1 Promise.all
async function loadMultiple(): Promise<[User, number, string]> {
const [user, count, msg] = await Promise.all([
fetchUser(3), // Promise<User>
Promise.resolve(42), // Promise<number>
Promise.resolve('完成') // Promise<string>
]);
return [user, count, msg];
}
Promise.all會回傳 一個陣列,其元素型別是傳入 Promise 的 交叉 (intersection),因此使用 元組 ([User, number, string]) 能得到精確型別。
6.2 Promise.race
function firstResponse<T>(promises: Promise<T>[]): Promise<T> {
return Promise.race(promises);
}
// 範例:先返回最快的 API 結果
firstResponse([fetchUser(4), fetchUser(5)])
.then((user) => console.log('先回應的使用者', user.id));
7. 自訂錯誤型別與 Promise<T, E>(非官方但常見做法)
class NotFoundError extends Error {
constructor(public readonly id: number) {
super(`Resource ${id} not found`);
this.name = 'NotFoundError';
}
}
/**
* 以 unknown 取代 any,讓錯誤處理更安全
*/
function safeFetchUser(id: number): Promise<User> {
return fetch(`https://api.example.com/users/${id}`)
.then((res) => {
if (res.status === 404) throw new NotFoundError(id);
if (!res.ok) throw new Error('Network error');
return res.json() as Promise<User>;
});
}
// 呼叫時使用 type guard
safeFetchUser(10)
.catch((err) => {
if (err instanceof NotFoundError) {
console.warn(`找不到 ID ${err.id}`);
} else {
console.error(err);
}
});
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 / 最佳實踐 |
|---|---|---|
忘記回傳 Promise |
在 async 函式中直接 return 非 Promise 物件會自動包裝,但在普通函式中常忘記 return new Promise,導致 undefined |
明確宣告 回傳型別 Promise<T>,若使用 async,則不必手動 new Promise |
使用 any 失去型別安全 |
reject 預設為 any,若不加以限制,catch 內會失去提示 |
使用 unknown 或自訂錯誤型別,配合 type guard |
then 中的型別推斷失敗 |
then 回傳的值若未明確指定,TS 可能推斷為 any |
在 then 回呼中 加上型別註記:promise.then((value: User) => ...) |
忘記 await |
在 async 函式內直接使用 promise,會得到 Promise<T> 而非 T,導致後續操作錯誤 |
始終使用 await 或在需要保留 Promise 時明確指出 Promise<T> |
| 未處理拒絕 (unhandled rejection) | Promise 被 reject 卻沒有 .catch 或 try/catch,會在執行階段拋出未捕獲錯誤 |
全局捕獲:process.on('unhandledRejection', ...)(Node)或 window.addEventListener('unhandledrejection', ...)(瀏覽器) |
| Promise.all 中的單一失敗導致全部失敗 | 任一 Promise reject,Promise.all 立即 reject,可能不是期望行為 |
使用 Promise.allSettled 或自行包裝 catch,保留每筆結果 |
最佳實踐清單
- 顯式宣告
Promise<T>:即使型別能被推斷,也建議寫出<T>,提升可讀性與維護性。 - 盡量使用
async / await:讓非同步流程看起來像同步程式,減少.then鏈的錯誤傳遞。 - 錯誤型別使用
unknown+ type guard:避免any帶來的隱性錯誤。 - 利用
Promise.allSettled處理多個請求的部分成功:在需要收集全部結果時更安全。 - 寫測試:使用 Jest 或 Vitest 的
async測試支援,確保 Promise 行為如預期。
實際應用場景
1. 前端資料快取(Cache)
type CacheEntry<T> = {
data: T;
expiry: number; // Unix timestamp
};
const userCache = new Map<number, CacheEntry<User>>();
async function getUserWithCache(id: number): Promise<User> {
const now = Date.now();
const cached = userCache.get(id);
if (cached && cached.expiry > now) {
return cached.data; // 直接回傳快取資料 (同步)
}
const fresh = await fetchUser(id);
userCache.set(id, { data: fresh, expiry: now + 5 * 60 * 1000 });
return fresh;
}
- 好處:
Promise<User>的型別讓快取與遠端呼叫的返回值保持一致,呼叫端不必關心資料來源。
2. Node.js 後端的資料庫查詢
import { Pool } from 'pg';
const pool = new Pool();
interface Post {
id: number;
title: string;
content: string;
}
/**
* 回傳 Promise<Post[]>,同時使用泛型保證結果結構
*/
function fetchPosts(): Promise<Post[]> {
return pool.query('SELECT * FROM posts')
.then(res => res.rows as Post[]);
}
// 在 Express route 中使用
app.get('/posts', async (req, res) => {
try {
const posts = await fetchPosts();
res.json(posts);
} catch (e) {
res.status(500).send('Database error');
}
});
- 型別一致:從資料庫取回的
rows直接斷言為Post[],若資料結構改變,編譯器會提醒相關使用處。
3. 多任務併發(限速)
/**
* 限制同時執行的 Promise 數量
*/
function throttle<T>(tasks: (() => Promise<T>)[], limit: number): Promise<T[]> {
const results: T[] = [];
let index = 0;
return new Promise((resolve, reject) => {
const run = () => {
if (index === tasks.length) {
if (results.length === tasks.length) resolve(results);
return;
}
const current = index++;
tasks[current]()
.then((value) => {
results[current] = value;
run(); // 啟動下一個
})
.catch(reject);
};
// 同時啟動 limit 個
for (let i = 0; i < Math.min(limit, tasks.length); i++) run();
});
}
// 使用範例
const fetchers = [1, 2, 3, 4, 5].map((id) => () => fetchUser(id));
throttle(fetchers, 2).then((users) => console.log('取得', users.length, '位使用者'));
- 核心:
throttle回傳Promise<T[]>,保證最終結果陣列的型別與輸入任務一致。
總結
Promise<T>為 非同步結果提供型別保證 的關鍵工具,讓 TypeScript 能在編譯期捕捉錯誤,提升程式碼品質。- 透過 顯式的泛型、
async / await、以及 適當的錯誤型別(unknown、自訂錯誤類別),開發者可以寫出 可讀、可維護且安全 的非同步程式。 - 常見陷阱(忘記
await、使用any、未處理 reject)只要遵守 最佳實踐清單,即可大幅降低錯誤機率。 - 在前端快取、後端資料庫存取、併發控制等實務情境中,
Promise<T>的型別資訊不僅讓 IDE 提供更好的自動完成,也讓團隊成員在閱讀程式碼時立刻了解非同步流程的輸入與輸出。
掌握了 Promise<T> 的概念與寫法後,你就能在 任何需要非同步操作的 TypeScript 專案 中,安全且高效地處理資料流,從而寫出更穩定、更具可擴充性的程式。祝你在 TypeScript 的非同步世界裡玩得開心! 🚀