本文 AI 產出,尚未審核

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,其主要功能是:

  • TPromise<U>(或多層嵌套的 Promise),則回傳 U(最內層的型別)。
  • T 不是 Promise,則直接回傳 T 本身。

簡單來說,它會 遞迴展開 Promise,讓你得到「最終」的值型別。

type Awaited<T> = 
  T extends PromiseLike<infer U> ? Awaited<U> : T;

:上述實作是 TypeScript 官方的簡化版,實際實作還考慮了 anynever 等特殊情況。

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 搭配時失去型別安全 TanyAwaited<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

最佳實踐

  1. 在函式回傳型別上使用type Res = Awaited<ReturnType<typeof fn>>,保證未來回傳變更仍能正確推斷。
  2. 配合 ReturnTypeParameters:組合使用可以建立 完整的 API 型別映射,如上例的 ResolvedApiMap
  3. 避免過度抽象:若僅在單一位置使用 await,不必額外建立 Awaited 型別,保持程式碼簡潔。
  4. 使用 unknown 而非 any:在需要接受不確定型別的函式參數時,用 unknown 搭配型別守衛,再用 Awaited 取得最終型別。
  5. 加入 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/fprxjs)會接受回傳 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 結合失去安全性等;最佳實踐則是與 ReturnTypeParameters 搭配使用、盡量避免 any、並在文件或註解中說明其意圖。
  • API 客戶端、函式庫設計、測試輔助、事件系統 等實務場景中,Awaited<T> 都能提升型別的表達力與維護性。

掌握 Awaited<T>,你就能在 TypeScript 專案中更自信地使用 async/await,寫出 型別安全、易於維護 的非同步程式碼。祝你開發順利,期待看到你在實務中運用這個強大的工具型別!