TypeScript – 異步與 Promise 型別(Async & Promise)
主題:async / await 型別推論
簡介
在現代前端與 Node.js 開發中,非同步流程已經是日常必備的技巧。
TypeScript 在提供靜態型別檢查的同時,也保留了 JavaScript 原生的 async / await 語法,讓開發者能以同步的寫法表達非同步邏輯。
然而,async 函式的回傳型別 不是普通的值,而是 Promise<T>,其中的 T 會由 TypeScript 自動推論。正確理解這個推論機制,能讓 IDE 給出更精確的自動完成、錯誤提示,並避免因型別不匹配而產生的執行時錯誤。本文將從基礎概念說明到實務範例,帶你掌握 async / await 的型別推論技巧。
核心概念
1. async 函式的回傳型別是 Promise<...>
當一個函式使用 async 關鍵字時,TypeScript 會自動將它的回傳型別包裝成 Promise<...>。
如果函式內直接回傳 一個值,則該值的型別會成為 Promise 的泛型參數 T。
async function getNumber(): number { // ❌ 錯誤寫法
return 42;
}
上例會產生編譯錯誤,因為 async 函式的實際回傳型別是 Promise<number>,而不是 number。正確寫法如下:
async function getNumber(): Promise<number> {
return 42; // 仍然回傳純數值,編譯器會自動包裝成 Promise
}
重點:即使你沒有在宣告中寫
: Promise<number>,TypeScript 也會推論出Promise<number>。
2. await 的型別推論
await 只能用在返回 Promise 的表達式上。它會「解開」Promise,取得內部的型別 T。
async function demo() {
const result = await getNumber(); // result 被推論為 number
// result 可以直接當作 number 使用
}
若 await 的目標不是 Promise(例如 await 5),TypeScript 仍會把它視為 Promise<5>,但會在編譯時產生警告,提醒你可能忘記包裝成 Promise。
3. 推論的限制:多重 Promise 和 any
(1) 多層 Promise
async function nested(): Promise<Promise<string>> {
return Promise.resolve('hello');
}
await 只會解開 一層:
async function test() {
const v = await nested(); // v 推論為 Promise<string>
}
如果想一次解開兩層,需要連續 await:
async function test() {
const v = await await nested(); // v 推論為 string
}
(2) any 與 unknown
當函式回傳 any 或 unknown 時,await 會保留原型別:
async function fetchData(): any {
return { id: 1 };
}
async function use() {
const data = await fetchData(); // data 為 any,失去型別安全
}
建議盡量避免 any,改用具體介面或 unknown 再自行斷言。
4. 型別推論與泛型結合
async 函式可以是 泛型,讓呼叫端決定 Promise 內的型別:
async function fetch<T>(url: string): Promise<T> {
const response = await fetchApi(url);
return response.json() as T; // 斷言回傳型別
}
// 使用範例
interface User { id: number; name: string; }
async function getUser(): Promise<User> {
return fetch<User>('/api/user');
}
此時 await fetch<User>(...) 的結果會被推論為 User,IDE 能提供完整的屬性補全。
程式碼範例
以下示範 5 個常見且實用的 async / await 型別推論情境,均附上說明註解。
範例 1:最簡單的回傳型別推論
// getAge.ts
async function getAge(): Promise<number> {
// 模擬非同步取得年齡
return 30; // 雖然回傳 number,編譯器會自動包成 Promise<number>
}
// 使用
async function showAge() {
const age = await getAge(); // age 被推論為 number
console.log(`Age: ${age}`);
}
說明:不寫
: Promise<number>也會得到相同型別,除非想要明確表達 API。
範例 2:從 Promise 物件解開型別
// userService.ts
interface User {
id: number;
name: string;
}
function fetchUser(id: number): Promise<User> {
return fetch(`/api/users/${id}`).then(res => res.json());
}
// async 包裝
async function getUserName(id: number): Promise<string> {
const user = await fetchUser(id); // user 被推論為 User
return user.name; // 回傳 string,最終回傳 Promise<string>
}
重點:
await讓user直接取得User型別,避免手動as User的斷言。
範例 3:泛型 async 函式
// genericFetch.ts
async function api<T>(endpoint: string): Promise<T> {
const response = await fetch(endpoint);
const data = await response.json();
return data as T; // 斷言回傳型別,呼叫端自行決定
}
// 呼叫端
interface Post {
id: number;
title: string;
}
async function loadPost(): Promise<Post> {
return api<Post>('/api/posts/1'); // 推論為 Promise<Post>
}
說明:透過泛型,
api函式可重用於任意資料結構,型別安全由呼叫端保證。
範例 4:多層 Promise 的解開
// doublePromise.ts
async function double(): Promise<Promise<number>> {
return Promise.resolve(100);
}
// 正確解開兩層
async function getValue() {
const inner = await double(); // inner: Promise<number>
const value = await inner; // value: number
console.log(value); // 100
}
提醒:
await只會解開 一層,若有嵌套Promise必須連續使用。
範例 5:避免 any 帶來的型別流失
// unsafeFetch.ts
async function unsafeFetch(): any {
return { foo: 'bar' };
}
// 錯誤示範
async function demo() {
const data = await unsafeFetch(); // data 為 any,失去型別檢查
console.log(data.baz); // 編譯器不會警告,但執行時會出錯
}
// 改寫為 unknown + 型別斷言
async function safeFetch(): Promise<unknown> {
return { foo: 'bar' };
}
async function demoSafe() {
const raw = await safeFetch(); // raw: unknown
if (typeof raw === 'object' && raw !== null) {
const obj = raw as { foo: string };
console.log(obj.foo); // 正確取得型別
}
}
最佳實踐:盡量避免
any,使用unknown搭配型別守衛,保留型別安全。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
忘記寫 Promise 回傳型別 |
編譯器會自動推論,但團隊閱讀時可能不易辨識。 | 明確標註 : Promise<T>,提升可讀性。 |
多層 Promise 未解開 |
只使用一次 await會得到 Promise<T>,導致後續操作失敗。 |
連續 await 或在函式內使用 return await 直接返回解開後的值。 |
any 造成型別流失 |
失去編譯時檢查,執行時容易出錯。 | 使用 unknown + 型別守衛,或在函式簽名中明確指定回傳型別。 |
await 非 Promise |
await 5 仍會編譯,但會產生隱含的 Promise.resolve(5),可能不是預期行為。 |
確認傳入 await 的是 真正的非同步函式,或手動包裝為 Promise。 |
| 錯誤處理遺漏 | async 函式拋出的錯誤會變成 rejected 的 Promise,若未捕獲會導致未處理的 rejection。 |
使用 try / catch 包住 await,或在呼叫端使用 .catch()。 |
最佳實踐摘要:
- 明確寫出
Promise<T>,即使 TypeScript 能推論。 - 只解開一層
Promise,必要時連續await。 - 避免
any,改用unknown搭配型別守衛。 - 統一錯誤處理:
try / catch+ 自訂錯誤型別。 - 善用泛型,讓 async 函式具備可重用性與型別安全。
實際應用場景
前端資料抓取
- 使用
api<T>泛型函式一次抓取多種資源(User、Post、Comment),型別自動推論,IDE 能即時提示屬性。
- 使用
Node.js 後端服務
- 在 Express 中的路由處理器使用
async (req, res) => {},回傳Promise<void>,可直接awaitDB 查詢,型別推論確保查詢結果正確。
- 在 Express 中的路由處理器使用
串接第三方 SDK
- 多層 Promise(如
stripe.paymentIntents.create().then(...).catch(...))可寫成async函式,一層await直接取得結果,減少回呼地獄。
- 多層 Promise(如
批次資料處理
- 透過
Promise.all配合async函式,型別會被推論為(T1 | T2 | …)[],讓後續的資料合併更安全。
- 透過
自訂錯誤類別
async函式拋出的錯誤可被型別化(throw new ValidationError()),在catch區塊中使用型別守衛,提升錯誤處理的可預測性。
總結
async函式的回傳型別永遠是Promise<T>,而await會把Promise<T>解開成T。- TypeScript 能自動推論這些型別,但在大型專案中,明確寫出型別、避免
any、正確解開多層 Promise,是提升可讀性與安全性的關鍵。 - 透過泛型、型別守衛與一致的錯誤處理,我們可以把非同步程式寫得像同步程式一樣直觀,同時保有 TypeScript 強大的型別保護。
掌握 async / await 的型別推論,不僅能讓程式碼更乾淨、錯誤更早被捕捉,也能在團隊合作時減少溝通成本。希望本篇文章能幫助你在日常開發中更自信地使用 TypeScript 的非同步特性,寫出更可靠、更易維護的程式碼。祝開發順利!