本文 AI 產出,尚未審核

TypeScript – 異步與 Promise 型別(Async & Promise)

主題:泛型 Promise


簡介

在前端與 Node.js 開發中,非同步流程是日常必備的技術。
Promise 為 ES6 引入的標準介面,讓我們可以以「成功」或「失敗」的狀態,清晰地描述非同步操作。

TypeScript 在 Promise 上加入了 泛型(generic)支援,使開發者能在編譯階段就捕捉到回傳資料的型別錯誤,提升程式的安全性與可維護性。
本篇文章將從概念說明、實作範例、常見陷阱到最佳實踐,完整介紹 泛型 Promise 的使用方式,幫助初學者快速上手,同時提供中級開發者進階的技巧。


核心概念

1. 為什麼要使用泛型 Promise?

Promise<T> 中的 T 代表 Promise 最終 resolve 時所返回的資料型別

function getUser(): Promise<User> { ... }

如果沒有寫泛型,Promise 會被視為 Promise<any>,編譯器無法幫助我們檢查 User 型別,容易產生隱蔽的錯誤。

重點:使用泛型可以在 編譯階段 捕捉型別不匹配,降低執行時的錯誤機率。


2. 基本語法

// 宣告一個回傳字串的 Promise
function fetchMessage(): Promise<string> {
  return new Promise<string>((resolve, reject) => {
    setTimeout(() => resolve('Hello, TypeScript!'), 1000);
  });
}
  • new Promise<T>(executor)executor 內的 resolve 參數會自動被推斷為 T
  • async 函式預設回傳 Promise<ReturnType>,如果明確寫出型別,會更具可讀性:
async function getNumber(): Promise<number> {
  return 42;   // 編譯器會自動包成 Promise<number>
}

3. 多層 Promise 與型別推斷

當 Promise 內再回傳 Promise 時,TypeScript 會 自動展開(flatten)最內層的型別:

function outer(): Promise<Promise<number>> {
  return Promise.resolve(Promise.resolve(10));
}

// 呼叫時仍得到 number
async function demo() {
  const n = await outer();   // n 被推斷為 number
}

這是因為 await 以及 .then 會把 Promise<Promise<T>> 轉成 Promise<T>


4. Promise.all 與泛型

Promise.all 接收一個 可迭代的 Promise 陣列,返回一個 泛型 Tuple,每個元素的型別對應原始陣列的順序:

const p1 = fetchMessage();          // Promise<string>
const p2 = getNumber();              // Promise<number>
const p3 = fetchUser();              // Promise<User>

async function combine() {
  const [msg, num, user] = await Promise.all([p1, p2, p3]);
  // msg: string, num: number, user: User
}

如果使用 Promise.allSettled,則回傳的型別會是 PromiseSettledResult<T>[],需要自行判斷 status


5. 自訂泛型 Promise 型別

在大型專案中,我們常會封裝一套 統一的回傳格式(例如 { code: number; data: T; msg: string }),此時可以自行定義一個泛型介面,並搭配 Promise

interface ApiResult<T> {
  code: number;
  data: T;
  msg: string;
}

// 例:取得商品列表
function fetchProducts(): Promise<ApiResult<Product[]>> {
  return fetch('/api/products')
    .then(res => res.json())
    .then(json => ({
      code: json.code,
      data: json.data as Product[],
      msg: json.msg,
    }));
}

這樣呼叫端只要寫:

async function showProducts() {
  const result = await fetchProducts();
  if (result.code === 0) {
    result.data.forEach(p => console.log(p.name));
  }
}

程式碼範例

以下提供 5 個實用範例,說明如何在不同情境下使用泛型 Promise。

範例 1:簡易的非同步計算

// 計算兩個數字的乘積,回傳 Promise<number>
function multiplyAsync(a: number, b: number): Promise<number> {
  return new Promise<number>((resolve) => {
    setTimeout(() => resolve(a * b), 500);
  });
}

// 使用 async/await
async function demoMultiply() {
  const result = await multiplyAsync(3, 7);
  console.log('乘積 =', result); // 乘積 = 21
}
demoMultiply();

說明Promise<number>result 的型別在編譯期即被確定為 number,不會因為 any 而失去 IntelliSense。


範例 2:從 API 取得 JSON 並映射型別

interface Post {
  id: number;
  title: string;
  body: string;
}

// 取得單一文章,回傳 Promise<Post>
function fetchPost(id: number): Promise<Post> {
  return fetch(`https://jsonplaceholder.typicode.com/posts/${id}`)
    .then(res => res.json() as Promise<Post>);
}

// 呼叫
async function showPost() {
  const post = await fetchPost(1);
  console.log(`#${post.id} ${post.title}`);
}
showPost();

重點as Promise<Post> 告訴 TypeScript 這段 JSON 會符合 Post 介面,避免 any


範例 3:自訂 API 回傳格式(泛型介面)

interface ApiResponse<T> {
  status: 'ok' | 'error';
  payload: T;
}

// 取得使用者資料,回傳 Promise<ApiResponse<User>>
function getUser(id: number): Promise<ApiResponse<User>> {
  return fetch(`/api/users/${id}`)
    .then(r => r.json())
    .then(data => ({
      status: data.status,
      payload: data.user as User,
    }));
}

// 使用
async function greetUser() {
  const resp = await getUser(5);
  if (resp.status === 'ok') {
    console.log(`嗨,${resp.payload.name}`);
  }
}

說明:透過 ApiResponse<T>,不同 API 只要改變 T 即可重用同一套型別定義。


範例 4:Promise.all 搭配不同型別

function delay<T>(value: T, ms: number): Promise<T> {
  return new Promise(resolve => setTimeout(() => resolve(value), ms));
}

async function parallelDemo() {
  const [msg, num, flag] = await Promise.all([
    delay('完成', 800),   // Promise<string>
    delay(123, 400),      // Promise<number>
    delay(true, 600),     // Promise<boolean>
  ]);

  // TypeScript 已正確推斷型別
  console.log(msg, num, flag); // 完成 123 true
}
parallelDemo();

技巧Promise.all 會自動產生 Tuple 型別,讓每個變數的型別皆被保留。


範例 5:錯誤處理與 Promise.reject

function fetchWithError(): Promise<string> {
  return new Promise<string>((_, reject) => {
    setTimeout(() => reject(new Error('網路錯誤')), 300);
  });
}

async function errorDemo() {
  try {
    const data = await fetchWithError();
    console.log(data);
  } catch (err) {
    // err 被推斷為 unknown,需自行斷言
    if (err instanceof Error) {
      console.error('捕獲錯誤:', err.message);
    }
  }
}
errorDemo();

提醒:即使 Promise 宣告了泛型,reject 的型別仍是 unknown,所以在 catch 中要做好型別斷言或使用 instanceof


常見陷阱與最佳實踐

陷阱 說明 解決方式
忘記寫泛型 Promise<any> 失去型別安全。 永遠Promise 加上 <T>,即使是 Promise<void>
resolve 傳入錯誤型別 resolve(123 as unknown as string) 會在編譯期通過,但執行時不符合預期。 讓編譯器自行推斷,或使用 as 前先確認型別。
await 直接套用在非 Promise await 5 會被包成 Promise<number>,但語意不清。 僅在確定回傳 Promise 時使用 await
Promise.all 中的 nullundefined 這兩者不會被視為 Promise,會直接作為結果返回,導致 Tuple 型別不一致。 確保陣列內每個元素皆為 Promise<T>,或使用 Promise.allSettled
錯誤類型為 unknown catch 區塊內的錯誤預設為 unknown,若直接存取 .message 會報錯。 使用 if (err instanceof Error) 進行型別守衛,或自行定義錯誤介面。

最佳實踐

  1. 明確標註返回型別function foo(): Promise<Result>,即使編譯器能推斷。
  2. 使用 async/await 搭配 try/catch,可讓錯誤流更清晰。
  3. 在大型專案中抽象 API 回傳:建立 ApiResponse<T>PagedResult<T> 等通用介面,減少重複程式碼。
  4. 避免在 Promise 內部做過多邏輯:保持 executor 簡潔,只負責「非同步」與「resolve/reject」的交接。
  5. 利用 TypeScript 的型別推斷:在 thencatchfinally 中不必重複寫型別,讓編譯器自行傳遞。

實際應用場景

  1. 前端資料抓取

    • 使用 fetch 取得 JSON,配合 Promise<ApiResponse<T>>,讓 UI 元件在取得資料前即可得到正確的型別提示,減少渲染錯誤。
  2. Node.js 後端服務

    • 讀寫資料庫(例如 mongodbsequelize)時,回傳 Promise<Model>,讓服務層可以直接使用模型屬性,避免手動 any 轉型。
  3. 第三方 SDK 包裝

    • 把原生 callback API 包裝成 Promise<T>,例如 fs.readFilereadFileAsync(path): Promise<Buffer>,讓使用者可以使用 await
  4. 批次處理與併發

    • 使用 Promise.all 同時發送多筆請求(如同時取得多個使用者資料),配合泛型 Tuple,讓每筆結果的型別都被保留。
  5. 錯誤統一管理

    • 建立 Result<T, E> 之類的泛型型別,讓成功與失敗的資訊都能在同一個 Promise 中回傳,提升錯誤處理的一致性。

總結

  • 泛型 Promise 為 TypeScript 提供了在非同步程式碼中「編譯期即驗證」的能力,讓開發者可以在 IDE 中即時得到型別提示與錯誤警告。
  • 正確使用 Promise<T>async/awaitPromise.all、自訂回傳介面,能大幅提升程式的可讀性、可維護性與安全性。
  • 避免常見陷阱(忘寫泛型、錯誤型別、混用 null/undefined),遵循最佳實踐,即可在實務專案中發揮 泛型 Promise 的最大威力。

掌握了這些概念與技巧,你就能在任何 TypeScript 專案中,以型別安全的方式處理非同步流程,寫出更穩定、易除錯的程式碼。祝開發順利!