本文 AI 產出,尚未審核

TypeScript

單元:異步與 Promise 型別(Async & Promise)

主題:Promise 型別(Promise<T>


簡介

在現代前端與 Node.js 開發中,非同步流程已經是不可或缺的一環。無論是呼叫遠端 API、讀寫檔案,還是處理計時器,都會涉及到 Promise
TypeScript 在 JavaScript 的 Promise 基礎上,加入了 泛型 (<T>) 讓開發者可以在編譯階段即掌握非同步結果的型別,從而大幅降低執行時錯誤的機會。

本單元將深入探討 Promise<T> 的概念、常見寫法與實務應用,並提供實用範例、陷阱與最佳實踐,幫助初學者快速上手,同時讓已有基礎的開發者更進一步提升型別安全與程式碼可讀性。


核心概念

1. Promise<T> 是什麼?

Promise<T> 表示 一個最終會解決 (resolve) 為型別 T 的非同步操作,或是被拒絕 (reject) 為 any(預設)。

// Promise<number> 最終會得到一個 number
declare const fetchUserAge: () => Promise<number>;
  • T:成功時的回傳值型別。
  • reject:若未指定,預設為 any,因此在 TypeScript 4.4+ 建議使用 Promise<T, E>(自訂錯誤型別)或透過 unknown 來加強安全性。

2. Promise 的三種狀態

狀態 說明
pending 初始狀態,尚未決定成功或失敗
fulfilled 呼叫 resolve(value)value 必須符合 T
rejected 呼叫 reject(reason)reason 為錯誤資訊

3. 基本語法與型別推斷

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

// 使用時,TypeScript 會自動推斷 Promise<void>
delay(1000).then(() => console.log('1 秒過去'));

重點:若不寫泛型,TS 會根據 resolve 的參數自行推斷 T。為了明確可讀,建議 顯式寫出 Promise<T>

4. 實作範例:從 API 取得使用者資料

interface User {
  id: number;
  name: string;
  email: string;
}

/**
 * 取得使用者資訊,回傳 Promise<User>
 */
function fetchUser(id: number): Promise<User> {
  return fetch(`https://api.example.com/users/${id}`)
    .then((res) => {
      if (!res.ok) throw new Error('Network response was not ok');
      return res.json() as Promise<User>; // 型別斷言
    });
}

// 呼叫方式
fetchUser(1)
  .then((user) => console.log(user.name))
  .catch((err) => console.error(err));

註解as Promise<User> 告訴編譯器 res.json() 會回傳符合 User 結構的物件。若想更安全,可使用 zodio-ts 等 runtime validation。

5. async / awaitPromise<T> 的結合

async 函式會自動把回傳值包裝成 Promise<T>,其中 T函式回傳值的型別

/**
 * 以 async/await 實作同樣的 fetchUser
 */
async function fetchUserAsync(id: number): Promise<User> {
  const response = await fetch(`https://api.example.com/users/${id}`);
  if (!response.ok) throw new Error('Network error');
  const data: User = await response.json(); // 型別推斷
  return data; // 隱含回傳 Promise<User>
}

// 使用
(async () => {
  try {
    const user = await fetchUserAsync(2);
    console.log(user.email);
  } catch (e) {
    console.error(e);
  }
})();

小技巧:在 async 函式內 直接使用型別註記(如 const data: User),可以在編譯階段捕捉 JSON 結構不符的錯誤。

6. 多個 Promise 的型別組合

6.1 Promise.all

async function loadMultiple(): Promise<[User, number, string]> {
  const [user, count, msg] = await Promise.all([
    fetchUser(3),               // Promise<User>
    Promise.resolve(42),        // Promise<number>
    Promise.resolve('完成')    // Promise<string>
  ]);
  return [user, count, msg];
}
  • Promise.all 會回傳 一個陣列,其元素型別是傳入 Promise 的 交叉 (intersection),因此使用 元組 ([User, number, string]) 能得到精確型別。

6.2 Promise.race

function firstResponse<T>(promises: Promise<T>[]): Promise<T> {
  return Promise.race(promises);
}

// 範例:先返回最快的 API 結果
firstResponse([fetchUser(4), fetchUser(5)])
  .then((user) => console.log('先回應的使用者', user.id));

7. 自訂錯誤型別與 Promise<T, E>(非官方但常見做法)

class NotFoundError extends Error {
  constructor(public readonly id: number) {
    super(`Resource ${id} not found`);
    this.name = 'NotFoundError';
  }
}

/**
 * 以 unknown 取代 any,讓錯誤處理更安全
 */
function safeFetchUser(id: number): Promise<User> {
  return fetch(`https://api.example.com/users/${id}`)
    .then((res) => {
      if (res.status === 404) throw new NotFoundError(id);
      if (!res.ok) throw new Error('Network error');
      return res.json() as Promise<User>;
    });
}

// 呼叫時使用 type guard
safeFetchUser(10)
  .catch((err) => {
    if (err instanceof NotFoundError) {
      console.warn(`找不到 ID ${err.id}`);
    } else {
      console.error(err);
    }
  });

常見陷阱與最佳實踐

陷阱 說明 解決方案 / 最佳實踐
忘記回傳 Promise async 函式中直接 return 非 Promise 物件會自動包裝,但在普通函式中常忘記 return new Promise,導致 undefined 明確宣告 回傳型別 Promise<T>,若使用 async,則不必手動 new Promise
使用 any 失去型別安全 reject 預設為 any,若不加以限制,catch 內會失去提示 使用 unknown 或自訂錯誤型別,配合 type guard
then 中的型別推斷失敗 then 回傳的值若未明確指定,TS 可能推斷為 any then 回呼中 加上型別註記
promise.then((value: User) => ...)
忘記 await async 函式內直接使用 promise,會得到 Promise<T> 而非 T,導致後續操作錯誤 始終使用 await 或在需要保留 Promise 時明確指出 Promise<T>
未處理拒絕 (unhandled rejection) Promise 被 reject 卻沒有 .catchtry/catch,會在執行階段拋出未捕獲錯誤 全局捕獲process.on('unhandledRejection', ...)(Node)或 window.addEventListener('unhandledrejection', ...)(瀏覽器)
Promise.all 中的單一失敗導致全部失敗 任一 Promise reject,Promise.all 立即 reject,可能不是期望行為 使用 Promise.allSettled 或自行包裝 catch,保留每筆結果

最佳實踐清單

  1. 顯式宣告 Promise<T>:即使型別能被推斷,也建議寫出 <T>,提升可讀性與維護性。
  2. 盡量使用 async / await:讓非同步流程看起來像同步程式,減少 .then 鏈的錯誤傳遞。
  3. 錯誤型別使用 unknown + type guard:避免 any 帶來的隱性錯誤。
  4. 利用 Promise.allSettled 處理多個請求的部分成功:在需要收集全部結果時更安全。
  5. 寫測試:使用 Jest 或 Vitest 的 async 測試支援,確保 Promise 行為如預期。

實際應用場景

1. 前端資料快取(Cache)

type CacheEntry<T> = {
  data: T;
  expiry: number; // Unix timestamp
};

const userCache = new Map<number, CacheEntry<User>>();

async function getUserWithCache(id: number): Promise<User> {
  const now = Date.now();
  const cached = userCache.get(id);
  if (cached && cached.expiry > now) {
    return cached.data; // 直接回傳快取資料 (同步)
  }
  const fresh = await fetchUser(id);
  userCache.set(id, { data: fresh, expiry: now + 5 * 60 * 1000 });
  return fresh;
}
  • 好處Promise<User> 的型別讓快取與遠端呼叫的返回值保持一致,呼叫端不必關心資料來源。

2. Node.js 後端的資料庫查詢

import { Pool } from 'pg';
const pool = new Pool();

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

/**
 * 回傳 Promise<Post[]>,同時使用泛型保證結果結構
 */
function fetchPosts(): Promise<Post[]> {
  return pool.query('SELECT * FROM posts')
    .then(res => res.rows as Post[]);
}

// 在 Express route 中使用
app.get('/posts', async (req, res) => {
  try {
    const posts = await fetchPosts();
    res.json(posts);
  } catch (e) {
    res.status(500).send('Database error');
  }
});
  • 型別一致:從資料庫取回的 rows 直接斷言為 Post[],若資料結構改變,編譯器會提醒相關使用處。

3. 多任務併發(限速)

/**
 * 限制同時執行的 Promise 數量
 */
function throttle<T>(tasks: (() => Promise<T>)[], limit: number): Promise<T[]> {
  const results: T[] = [];
  let index = 0;

  return new Promise((resolve, reject) => {
    const run = () => {
      if (index === tasks.length) {
        if (results.length === tasks.length) resolve(results);
        return;
      }
      const current = index++;
      tasks[current]()
        .then((value) => {
          results[current] = value;
          run(); // 啟動下一個
        })
        .catch(reject);
    };

    // 同時啟動 limit 個
    for (let i = 0; i < Math.min(limit, tasks.length); i++) run();
  });
}

// 使用範例
const fetchers = [1, 2, 3, 4, 5].map((id) => () => fetchUser(id));
throttle(fetchers, 2).then((users) => console.log('取得', users.length, '位使用者'));
  • 核心throttle 回傳 Promise<T[]>,保證最終結果陣列的型別與輸入任務一致。

總結

  • Promise<T>非同步結果提供型別保證 的關鍵工具,讓 TypeScript 能在編譯期捕捉錯誤,提升程式碼品質。
  • 透過 顯式的泛型async / await、以及 適當的錯誤型別unknown、自訂錯誤類別),開發者可以寫出 可讀、可維護且安全 的非同步程式。
  • 常見陷阱(忘記 await、使用 any、未處理 reject)只要遵守 最佳實踐清單,即可大幅降低錯誤機率。
  • 在前端快取、後端資料庫存取、併發控制等實務情境中,Promise<T> 的型別資訊不僅讓 IDE 提供更好的自動完成,也讓團隊成員在閱讀程式碼時立刻了解非同步流程的輸入與輸出。

掌握了 Promise<T> 的概念與寫法後,你就能在 任何需要非同步操作的 TypeScript 專案 中,安全且高效地處理資料流,從而寫出更穩定、更具可擴充性的程式。祝你在 TypeScript 的非同步世界裡玩得開心! 🚀