TypeScript 教學:深入了解 Utility Type Awaited<T>
簡介
在現代前端與後端開發中,非同步程式已成為日常。
TypeScript 為了讓開發者在編寫 async/await 時能夠得到正確的型別推斷,提供了 Utility Type Awaited<T>。
Awaited<T> 能夠「抽取」Promise 內部的真正值型別,讓你在函式簽名、泛型工具或是型別推斷時,能夠以 同理 的方式處理非同步結果。
掌握它不只可以避免型別錯誤,還能提升程式碼的 可讀性 與 可維護性,尤其在大型專案或是函式庫開發時,這點尤為重要。
本文將從概念說明、實作範例、常見陷阱到最佳實踐,完整帶你了解 Awaited<T>,並提供實務應用的參考。
核心概念
1. Awaited<T> 是什麼?
Awaited<T> 是 TypeScript 內建的 Utility Type,其主要功能是:
- 若
T為Promise<U>(或多層嵌套的Promise),則回傳U(最內層的型別)。 - 若
T不是Promise,則直接回傳T本身。
簡單來說,它會 遞迴展開 Promise,讓你得到「最終」的值型別。
type Awaited<T> =
T extends PromiseLike<infer U> ? Awaited<U> : T;
註:上述實作是 TypeScript 官方的簡化版,實際實作還考慮了
any、never等特殊情況。
2. 為什麼需要 Awaited<T>?
在沒有 Awaited<T> 前,我們通常會寫類似以下的型別:
type Resolve<T> = T extends Promise<infer R> ? R : T;
但這樣只能處理 單層 Promise,若遇到 Promise<Promise<string>> 或自訂的 Thenable,就會失效。Awaited<T> 透過遞迴的方式,無論 Promise 嵌套多少層,都能正確取得最終值型別。
3. Awaited<T> 與 async/await 的關係
await 表達式在執行時會自動解包 Promise,而 Awaited<T> 正是型別層面的「解包」對應。
在以下函式中:
async function fetchUser(): Promise<User> { /* ... */ }
const user = await fetchUser(); // user 的型別是 User
若我們想要在 型別層面 表示 await fetchUser() 的結果,可以寫成:
type Result = Awaited<ReturnType<typeof fetchUser>>; // Result === User
這樣即使 fetchUser 的回傳型別改變,Result 仍會自動跟著更新。
程式碼範例
以下示範 5 個實用情境,說明 Awaited<T> 的用法與好處。
範例 1:簡單的 Promise 解包
// 假設有一個 API 回傳 Promise<number>
function getCount(): Promise<number> {
return Promise.resolve(42);
}
// 使用 Awaited 取得解包後的型別
type Count = Awaited<ReturnType<typeof getCount>>; // Count 為 number
// 應用在變數宣告上
let total: Count = 0; // ✅ 正確型別
說明:
ReturnType<typeof getCount>取得getCount的回傳型別Promise<number>,Awaited<>再把它展開成number。
範型 2:多層嵌套的 Promise
function nested(): Promise<Promise<string>> {
return Promise.resolve(Promise.resolve('hello'));
}
// 直接使用 ReturnType 只能得到 Promise<Promise<string>>
type Raw = ReturnType<typeof nested>; // Promise<Promise<string>>
// 用 Awaited 完整展開
type Flatten = Awaited<Raw>; // string
const msg: Flatten = 'world'; // ✅
說明:即使
Promise被套了兩層,Awaited仍能遞迴取得最內層的string。
範例 3:自訂的 Thenable(類似 Promise)
class MyThenable<T> {
constructor(private value: T) {}
then(callback: (v: T) => void) {
callback(this.value);
}
}
// MyThenable 也是 PromiseLike,Awaited 能處理
type Value = Awaited<MyThenable<boolean>>; // boolean
function useThenable(t: MyThenable<boolean>): Awaited<typeof t> {
// 直接回傳解包後的型別
return true; // ✅ 符合 boolean
}
說明:只要物件符合
PromiseLike(具備then方法),Awaited皆可正確展開。
範例 4:在泛型工具型別中使用 Awaited
// 定義一個接受非同步函式的高階函式
function wrapAsync<F extends (...args: any[]) => any>(fn: F) {
return async (...args: Parameters<F>): Promise<Awaited<ReturnType<F>>> => {
// 直接呼叫原函式,回傳值會自動被 Awaited 包裝
return await fn(...args);
};
}
// 原始函式回傳 Promise<number>
async function fetchScore(id: string): Promise<number> {
return 100;
}
// 包裝後的函式仍保留正確的回傳型別
const wrappedFetch = wrapAsync(fetchScore);
type WrappedReturn = Awaited<ReturnType<typeof wrappedFetch>>; // number
說明:
wrapAsync透過Awaited<ReturnType<F>>讓呼叫者在型別層面感受到「已解包」的結果,避免二次await的型別混淆。
範例 5:配合條件型別與映射型別的實務應用
假設有一個 API 定義集合,我們想要產生 全部解包後的回傳型別:
type ApiMap = {
getUser: () => Promise<{ id: number; name: string }>;
getPosts: (uid: number) => Promise<Array<{ id: number; title: string }>>;
ping: () => Promise<void>;
};
// 產生解包後的型別映射
type ResolvedApiMap = {
[K in keyof ApiMap]: Awaited<ReturnType<ApiMap[K]>>;
};
/*
結果:
type ResolvedApiMap = {
getUser: { id: number; name: string };
getPosts: Array<{ id: number; title: string }>;
ping: void;
}
*/
function callApi<K extends keyof ApiMap>(key: K, ...args: Parameters<ApiMap[K]>) {
// 這裡的返回值會自動是解包後的型別
return (apiImpl[key] as any)(...args) as Promise<ResolvedApiMap[K]>;
}
說明:透過
Awaited搭配映射型別,我們一次性得到所有 API 的「最終」回傳型別,讓後續的使用者不必自行await再推斷型別。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
忘記 Awaited 只在型別層面 |
Awaited<T> 不會 在執行時自動 await,它只是型別工具。 |
仍需在程式碼中使用 await 或 .then(),僅在型別宣告時使用 Awaited。 |
與 any 搭配時失去型別安全 |
若 T 為 any,Awaited<any> 仍是 any,會失去檢查。 |
盡量避免在公共 API 中使用 any,改用 unknown 再配合型別守衛。 |
| 遞迴過深導致編譯效能下降 | 在極端情況(如 Promise<Promise<...>> 超過 10 層)會增加型別計算成本。 |
大多數實務情況不會出現這種深度,若真的需要,可考慮手動拆解或使用 type 別名限制深度。 |
錯誤假設 Awaited 會處理同步值 |
Awaited<number> 仍是 number,不會變成 Promise<number>。 |
只在需要「從 Promise 取得值」的情境使用,避免把同步值包成 Promise。 |
與 void 結合的誤解 |
Awaited<Promise<void>> 結果是 void,但 void 在 TypeScript 中可接受 undefined。 |
若需要嚴格的 void(不允許 undefined),可自行建立 NonUndefinedVoid = undefined extends void ? never : void。 |
最佳實踐
- 在函式回傳型別上使用:
type Res = Awaited<ReturnType<typeof fn>>,保證未來回傳變更仍能正確推斷。 - 配合
ReturnType、Parameters:組合使用可以建立 完整的 API 型別映射,如上例的ResolvedApiMap。 - 避免過度抽象:若僅在單一位置使用
await,不必額外建立Awaited型別,保持程式碼簡潔。 - 使用
unknown而非any:在需要接受不確定型別的函式參數時,用unknown搭配型別守衛,再用Awaited取得最終型別。 - 加入 JSDoc 或註解:說明
Awaited的目的,讓團隊成員快速了解型別背後的非同步意圖。
實際應用場景
1. API 客戶端庫
在開發大型的 REST 或 GraphQL 客戶端時,往往會把每個端點的回傳型別定義成 Promise<...>。
使用 Awaited 可以一次性產生「已解包」的型別,供 UI 層直接使用,減少 await 的型別噪音。
// client.ts
export async function request<T>(url: string): Promise<T> {
const res = await fetch(url);
return (await res.json()) as T;
}
// UI component
type User = Awaited<ReturnType<typeof request<User>>>; // User
2. 高階函式 (Higher‑Order Functions)
如上 wrapAsync 範例,許多函式庫(如 lodash/fp、rxjs)會接受回傳 Promise 的函式並返回新函式。Awaited 讓這類高階函式在型別上保持 透明,使用者不必自行寫 Promise<Awaited<...>>。
3. 測試框架的型別斷言
在 Jest、Vitest 等測試框架中,常會寫:
await expect(fetchData()).resolves.toEqual(expected);
若要在自訂的測試輔助函式中取得 fetchData 的最終結果型別,Awaited 是最直接的方式:
function expectResolved<T>(promise: Promise<T>) {
return expect(promise).resolves as unknown as Awaited<Promise<T>>;
}
4. 型別安全的事件系統
事件處理函式有時會返回 Promise<void>,但在事件分派器裡,我們想保證所有回傳值都已被解包:
type Listener<E> = (event: E) => Promise<void>;
class EventBus<E> {
private listeners: Listener<E>[] = [];
on(l: Listener<E>) { this.listeners.push(l); }
async emit(event: E) {
await Promise.all(this.listeners.map(l => l(event)));
}
// 取得所有 listener 的返回型別(應該都是 void)
type ListenerResult = Awaited<ReturnType<Listener<E>>>; // void
}
總結
Awaited<T>是 TypeScript 內建的 Utility Type,專門用來 遞迴展開Promise(或PromiseLike)的最終值型別。- 它在 型別層面 完全對應
await的行為,使得在函式回傳、泛型工具、API 映射等情境下,能夠保持型別的 正確性 與 可讀性。 - 透過實務範例,我們看到
Awaited能解決多層 Promise、Thenable、以及高階函式等常見需求。 - 常見的陷阱包括誤以為
Awaited會在執行時自動await、與any結合失去安全性等;最佳實踐則是與ReturnType、Parameters搭配使用、盡量避免any、並在文件或註解中說明其意圖。 - 在 API 客戶端、函式庫設計、測試輔助、事件系統 等實務場景中,
Awaited<T>都能提升型別的表達力與維護性。
掌握 Awaited<T>,你就能在 TypeScript 專案中更自信地使用 async/await,寫出 型別安全、易於維護 的非同步程式碼。祝你開發順利,期待看到你在實務中運用這個強大的工具型別!