本文 AI 產出,尚未審核

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. 推論的限制:多重 Promiseany

(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) anyunknown

當函式回傳 anyunknown 時,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>
}

重點awaituser 直接取得 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 + 型別守衛,或在函式簽名中明確指定回傳型別。
awaitPromise await 5 仍會編譯,但會產生隱含的 Promise.resolve(5),可能不是預期行為。 確認傳入 await 的是 真正的非同步函式,或手動包裝為 Promise
錯誤處理遺漏 async 函式拋出的錯誤會變成 rejected 的 Promise,若未捕獲會導致未處理的 rejection。 使用 try / catch 包住 await,或在呼叫端使用 .catch()

最佳實踐摘要

  1. 明確寫出 Promise<T>,即使 TypeScript 能推論。
  2. 只解開一層 Promise,必要時連續 await
  3. 避免 any,改用 unknown 搭配型別守衛。
  4. 統一錯誤處理try / catch + 自訂錯誤型別。
  5. 善用泛型,讓 async 函式具備可重用性與型別安全。

實際應用場景

  1. 前端資料抓取

    • 使用 api<T> 泛型函式一次抓取多種資源(User、Post、Comment),型別自動推論,IDE 能即時提示屬性。
  2. Node.js 後端服務

    • 在 Express 中的路由處理器使用 async (req, res) => {},回傳 Promise<void>,可直接 await DB 查詢,型別推論確保查詢結果正確。
  3. 串接第三方 SDK

    • 多層 Promise(如 stripe.paymentIntents.create().then(...).catch(...))可寫成 async 函式,一層 await 直接取得結果,減少回呼地獄。
  4. 批次資料處理

    • 透過 Promise.all 配合 async 函式,型別會被推論為 (T1 | T2 | …)[],讓後續的資料合併更安全。
  5. 自訂錯誤類別

    • async 函式拋出的錯誤可被型別化(throw new ValidationError()),在 catch 區塊中使用型別守衛,提升錯誤處理的可預測性。

總結

  • async 函式的回傳型別永遠是 Promise<T>,而 await 會把 Promise<T> 解開成 T
  • TypeScript 能自動推論這些型別,但在大型專案中,明確寫出型別避免 any正確解開多層 Promise,是提升可讀性與安全性的關鍵。
  • 透過泛型、型別守衛與一致的錯誤處理,我們可以把非同步程式寫得像同步程式一樣直觀,同時保有 TypeScript 強大的型別保護。

掌握 async / await 的型別推論,不僅能讓程式碼更乾淨、錯誤更早被捕捉,也能在團隊合作時減少溝通成本。希望本篇文章能幫助你在日常開發中更自信地使用 TypeScript 的非同步特性,寫出更可靠、更易維護的程式碼。祝開發順利!