本文 AI 產出,尚未審核

TypeScript 教學:深入了解 Utility Type ReturnType<T>

簡介

在大型的 TypeScript 專案中,函式的回傳型別往往會因需求變更而頻繁調整。若每次都手動同步型別,既容易遺漏也會造成維護成本激增。ReturnType<T> 正是為了讓開發者能自動抽取函式的回傳型別,避免重複寫型別定義,提升程式碼的可讀性與安全性。

本篇文章將從概念說明、實作範例、常見陷阱到最佳實踐,完整帶你掌握 ReturnType<T> 的使用方式,讓你在日常開發中能更自信地處理型別推斷與重構。


核心概念

什麼是 ReturnType<T>?

ReturnType<T> 是 TypeScript 內建的 Utility Type,它接受一個函式型別 T(或可呼叫的類型),並回傳該函式的 回傳型別。語法如下:

type ReturnType<T extends (...args: any) => any> = /* 內部實作 */

簡單來說,給它一個函式 fooReturnType<typeof foo> 會變成 foo 的回傳型別。這讓我們可以在不手動寫出回傳型別的情況下,直接取得並在其他地方重用。

為什麼需要它?

  • 避免型別重複:當函式回傳型別改變時,只要修改函式本身,所有使用 ReturnType 的地方會自動更新。
  • 增強可讀性:型別名稱直接說明「這是某函式的回傳型別」,比起自行寫 type X = ... 更直觀。
  • 支援高階函式:在函式組合或柯里化(currying)時,常需要取得內層函式的回傳型別,ReturnType 可以輕鬆完成。

基本範例

function getUser(id: number) {
  return { id, name: "Alice", age: 30 };
}

// 直接取得回傳型別
type User = ReturnType<typeof getUser>;

const u: User = { id: 1, name: "Bob", age: 25 }; // ✅ 正確

在上例中,我們不需要手動寫 type User = { id: number; name: string; age: number; },而是透過 ReturnType 直接抽取。


程式碼範例

以下示範 5 個實用情境,從最簡單到稍微進階的應用,幫助你快速上手。

1️⃣ 基本函式回傳型別抽取

// 範例函式
function fetchData(url: string) {
  return fetch(url).then(res => res.json());
}

// 取得回傳型別
type FetchResult = ReturnType<typeof fetchData>;

// 使用
let data: FetchResult;
fetchData('https://api.example.com')
  .then(result => {
    data = result; // result 的型別自動對應
  });

重點FetchResultPromise<any>,若想進一步取得 json() 的結果型別,可結合 Awaited<...>(在 TypeScript 4.5 之後支援)。

2️⃣ 高階函式:包裝器 (Wrapper)

// 包裝函式,接受任意函式並回傳相同回傳值
function withLogging<T extends (...args: any[]) => any>(fn: T) {
  return (...args: Parameters<T>): ReturnType<T> => {
    console.log('呼叫參數:', args);
    const result = fn(...args);
    console.log('回傳結果:', result);
    return result;
  };
}

// 原始函式
function add(a: number, b: number) {
  return a + b;
}

// 使用 withLogging
const addLogged = withLogging(add);
const sum = addLogged(3, 4); // sum 的型別是 number

在此範例中,我們利用 ReturnType<T>withLogging 能正確推斷被包裝函式的回傳型別,確保 addLogged 的簽名與原始 add 完全相同。

3️⃣ 結合 Awaited 取得 Promise 裡的實際型別

async function getConfig() {
  return { debug: true, version: "1.2.3" };
}

// 直接使用 ReturnType 會得到 Promise<{...}>
type ConfigPromise = ReturnType<typeof getConfig>;

// 想要取得解包後的型別
type Config = Awaited<ConfigPromise>;

const cfg: Config = { debug: false, version: "2.0.0" }; // ✅ 正確

Awaited<T> 是 TypeScript 4.5+ 的新工具型別,與 ReturnType 搭配使用,可一次取得 async 函式最終的資料型別。

4️⃣ 取出類別方法的回傳型別

class Service {
  fetch(id: string) {
    return { id, payload: "data" };
  }
}

// 取得類別方法的回傳型別
type FetchResult = ReturnType<Service["fetch"]>;

const r: FetchResult = { id: "abc", payload: "data" }; // ✅ 正確

透過索引型別 Service["fetch"],我們直接取得 fetch 方法的型別,再套用 ReturnType,非常適合在介面或抽象類別中重用型別。

5️⃣ 與條件型別結合:建立「函式回傳型別映射」

type ApiMap = {
  getUser: (id: number) => Promise<{ id: number; name: string }>;
  getPosts: (userId: number) => Promise<Array<{ id: number; title: string }>>;
};

// 產生每個 API 的回傳型別
type ApiResult<K extends keyof ApiMap> = Awaited<ReturnType<ApiMap[K]>>;

// 使用
type User = ApiResult<"getUser">;          // { id: number; name: string }
type Posts = ApiResult<"getPosts">;       // Array<{ id: number; title: string }>

這個技巧常見於 API client 的型別設計,能讓開發者只需提供 API 名稱,即可取得對應的回傳型別,減少手寫映射表的工作。


常見陷阱與最佳實踐

陷阱 說明 解決方式
傳入非函式型別 ReturnType<T> 要求 T 必須是可呼叫的型別,若傳入普通物件會報錯。 使用 T extends (...args: any) => any 限制,或先確保 typeof fn === "function"
取得 any 若函式沒有明確回傳型別(例如直接 return;),ReturnType 會推斷為 voidany 為函式加上顯式回傳型別,或在需要時使用 unknown 取代 any
Promise 包裝 直接使用 ReturnType 取得 async 函式的回傳型別會是 Promise<T>,有時需要解包。 搭配 Awaited<T> 或自行寫 type UnwrapPromise<T> = T extends Promise<infer R> ? R : T;
泛型函式 若函式本身是泛型,ReturnType 會返回一個 帶有泛型參數的函式型別,而非具體回傳型別。 先對泛型參數具體化(例如 typeof fn<string>)或使用條件型別取得特定實例。
過度抽象 在簡單情況下過度使用 ReturnType 會讓程式碼可讀性下降。 只在 需要重用或自動同步 的情境使用,保持程式碼的直觀性。

最佳實踐

  1. 保持函式簽名簡潔
    • 為每個公開函式加上明確的回傳型別,讓 ReturnType 能正確推斷。
  2. 搭配 Parameters<T> 使用
    • 若需要同時取得參數與回傳型別,使用 Parameters<T> + ReturnType<T>,可寫出完整的高階函式型別。
  3. 在 API client 中建立型別映射
    • 如上例 5 所示,使用條件型別與 ReturnType 結合,讓 API 回傳型別自動更新。
  4. 使用 as const 固定字面量
    • 若回傳值是字面量物件,配合 as const 可讓 ReturnType 推斷出更精確的型別(如字串聯合類型)。
  5. 測試型別變更
    • 在 CI 中加入 tsc --noEmit,確保因函式回傳型別變更而導致的錯誤能即時捕捉。

實際應用場景

1️⃣ Redux / Zustand State Selector

在狀態管理庫中,selector 函式會根據全域狀態回傳子集合。使用 ReturnType 可以自動推斷 selector 的回傳型別,避免手寫冗長的型別宣告。

type RootState = { user: { id: number; name: string }; theme: "light" | "dark" };

const selectUser = (state: RootState) => state.user;

// selector 回傳型別自動取得
type UserSlice = ReturnType<typeof selectUser>;

2️⃣ 服務層(Service Layer)自動生成 DTO

在微服務或後端 API 開發時,常需要根據服務方法產生資料傳輸物件(DTO)。ReturnType 可直接作為 DTO 定義,保證前後端型別一致。

class OrderService {
  async createOrder(data: { productId: number; qty: number }) {
    // ... 實作
    return { orderId: "A001", status: "pending" } as const;
  }
}

// DTO 型別
type CreateOrderResult = Awaited<ReturnType<OrderService["createOrder"]>>;

// 前端直接引用
function handleCreate(result: CreateOrderResult) {
  console.log(result.orderId);
}

3️⃣ 測試輔助函式

在單元測試中,我們常需要根據被測函式的回傳值建立 mock。ReturnType 可協助生成正確型別的 mock 物件,減少測試程式碼與實作脫節。

function computeStats(nums: number[]) {
  return { avg: 0, max: 0, min: 0 };
}

// 測試用 mock
type Stats = ReturnType<typeof computeStats>;

const mockStats: Stats = { avg: 5, max: 10, min: 1 };

4️⃣ 動態生成 UI 元件的 Props

在 React + TypeScript 專案裡,若有函式 getComponentProps 產生元件屬性,使用 ReturnType 可以直接將其作為元件的 Props 型別,避免手寫重複。

function getButtonProps(label: string) {
  return { children: label, type: "button" as const };
}

type ButtonProps = ReturnType<typeof getButtonProps>;

const MyButton: React.FC<ButtonProps> = (props) => <button {...props} />;

總結

  • ReturnType<T> 是 TypeScript 強大的 Utility Type,讓我們能自動抽取函式的回傳型別。
  • 透過 ReturnType 搭配 Parameters, Awaited, 以及條件型別,我們可以在高階函式、API 客戶端、狀態管理與測試等多種場景中保持型別一致性。
  • 使用時需注意傳入的必須是可呼叫的型別、async 函式會回傳 Promise<T>,以及泛型函式的特殊處理。
  • 最佳實踐包括:為函式加上明確回傳型別、在需要重用時才使用 ReturnType、結合 as const 取得更精確的字面量型別、以及在 CI 中持續檢查型別變更。

掌握 ReturnType<T> 後,你將能寫出更安全可維護易於重構的 TypeScript 程式碼,讓團隊在面對需求變更時不再為型別同步問題苦惱。祝你在實務開發中玩得開心,寫出更好、更乾淨的程式!