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> = /* 內部實作 */
簡單來說,給它一個函式 foo,ReturnType<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 的型別自動對應
});
重點:
FetchResult為Promise<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 會推斷為 void 或 any。 |
為函式加上顯式回傳型別,或在需要時使用 unknown 取代 any。 |
| Promise 包裝 | 直接使用 ReturnType 取得 async 函式的回傳型別會是 Promise<T>,有時需要解包。 |
搭配 Awaited<T> 或自行寫 type UnwrapPromise<T> = T extends Promise<infer R> ? R : T;。 |
| 泛型函式 | 若函式本身是泛型,ReturnType 會返回一個 帶有泛型參數的函式型別,而非具體回傳型別。 |
先對泛型參數具體化(例如 typeof fn<string>)或使用條件型別取得特定實例。 |
| 過度抽象 | 在簡單情況下過度使用 ReturnType 會讓程式碼可讀性下降。 |
只在 需要重用或自動同步 的情境使用,保持程式碼的直觀性。 |
最佳實踐
- 保持函式簽名簡潔
- 為每個公開函式加上明確的回傳型別,讓
ReturnType能正確推斷。
- 為每個公開函式加上明確的回傳型別,讓
- 搭配
Parameters<T>使用- 若需要同時取得參數與回傳型別,使用
Parameters<T>+ReturnType<T>,可寫出完整的高階函式型別。
- 若需要同時取得參數與回傳型別,使用
- 在 API client 中建立型別映射
- 如上例 5 所示,使用條件型別與
ReturnType結合,讓 API 回傳型別自動更新。
- 如上例 5 所示,使用條件型別與
- 使用
as const固定字面量- 若回傳值是字面量物件,配合
as const可讓ReturnType推斷出更精確的型別(如字串聯合類型)。
- 若回傳值是字面量物件,配合
- 測試型別變更
- 在 CI 中加入
tsc --noEmit,確保因函式回傳型別變更而導致的錯誤能即時捕捉。
- 在 CI 中加入
實際應用場景
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 程式碼,讓團隊在面對需求變更時不再為型別同步問題苦惱。祝你在實務開發中玩得開心,寫出更好、更乾淨的程式!