TypeScript – 函式參數與回傳值型別
簡介
在 JavaScript 中,函式的參數與回傳值是動態、無型別的,這讓開發者在大型專案裡容易因為型別不一致而產生難以追蹤的錯誤。TypeScript 透過靜態型別檢查,讓我們在編譯階段就能發現這類問題,提升程式的可讀性與維護性。
本章節聚焦於 函式參數與回傳值的型別,從基本寫法、預設值、可選參數、剩餘參數,到函式型別別名與泛型的實作,逐步帶你建立完整的型別觀念。掌握這些概念後,你將能寫出更安全、更具自文件(self‑documenting)的程式碼。
核心概念
1. 基本參數與回傳型別
在 TypeScript 中,只要在參數或函式宣告後加上型別註記,就能讓編譯器檢查傳入與回傳的資料是否符合預期。
// 參數 a 必須是 number,回傳值也必須是 number
function add(a: number, b: number): number {
return a + b;
}
// 正確呼叫
const sum = add(3, 5); // sum 的型別被推斷為 number
// 錯誤呼叫 (編譯時會報錯)
// const bad = add('3', 5);
重點:若省略回傳型別,TypeScript 會根據函式內容自動推斷。但在公共 API 中,明確宣告回傳型別 能提升可讀性與未來維護的安全性。
2. 可選參數 (?) 與預設值 (=)
2.1 可選參數
在參數名稱後加上 ?,代表呼叫時可以省略該參數,未傳入時其型別會被視為 undefined。
function greet(name: string, title?: string): string {
// title 可能是 undefined,需要先檢查
return title ? `Hello, ${title} ${name}` : `Hello, ${name}`;
}
greet('Alice'); // "Hello, Alice"
greet('Bob', 'Dr.'); // "Hello, Dr. Bob"
2.2 預設值
給參數指定預設值,等同於「可選且如果未提供就使用預設值」。此時參數的型別會自動變為 該型別或 undefined,除非使用 strictNullChecks 時明確排除 undefined。
function multiply(a: number, b: number = 2): number {
return a * b;
}
multiply(5); // 10 (使用預設值 2)
multiply(5, 3); // 15
技巧:若同時使用
?與預設值,?其實是多餘的,因為預設值已隱含「可選」的語意。
3. 剩餘參數 (...)
剩餘參數允許函式接受不定數量的參數,最常與陣列操作結合使用。其型別必須寫成陣列型別。
function join(separator: string, ...items: string[]): string {
return items.join(separator);
}
const result = join(', ', 'apple', 'banana', 'cherry');
// result => "apple, banana, cherry"
注意:剩餘參數必須是最後一個參數,且只能有一個。
4. 函式型別別名與介面 (type / interface)
在大型程式中,我們常需要把「函式的簽名」抽離成型別,方便重複使用或作為參數傳遞。
// 使用 type 定義函式型別
type Comparator<T> = (a: T, b: T) => number;
// 以型別別名作為參數
function sort<T>(arr: T[], compare: Comparator<T>): T[] {
return arr.slice().sort(compare);
}
// 使用範例
const numbers = [5, 2, 9, 1];
const sorted = sort(numbers, (x, y) => x - y);
// sorted => [1, 2, 5, 9]
如果想要在介面中加入函式屬性,也可以:
interface Logger {
(level: 'info' | 'warn' | 'error', message: string): void;
}
const consoleLogger: Logger = (level, msg) => {
console[level](msg);
};
consoleLogger('info', '程式啟動'); // 等同於 console.info('程式啟動')
5. 泛型函式 (Generics)
泛型讓函式在保持型別安全的同時,能夠接受多種不同型別的參數與回傳值。
// 基本的 identity 函式
function identity<T>(value: T): T {
return value;
}
const num = identity(42); // num 推斷為 number
const str = identity('hello'); // str 推斷為 string
5.1 泛型約束 (extends)
有時候我們希望限制泛型只能是某些型別的子集合,使用 extends 來設定約束。
function getLength<T extends { length: number }>(obj: T): number {
return obj.length;
}
getLength('abc'); // 3
getLength([1, 2, 3, 4]); // 4
// getLength(123); // 編譯錯誤,number 沒有 length 屬性
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
| 忘記標註回傳型別 | 在公共函式中省略回傳型別,可能因推斷錯誤導致不易發現的 bug。 | 明確寫出回傳型別,尤其是 void、Promise<T> 等。 |
| 可選參數放在必填參數之後 | 參數順序錯誤會使呼叫時的定位失敗,編譯器會報錯。 | 將所有可選參數放在最後,或使用預設值取代可選參數。 |
| 剩餘參數型別寫錯 | 把剩餘參數寫成單一型別而非陣列,會失去陣列方法的型別支援。 | 使用 ...args: Type[],如 ...nums: number[]。 |
| 泛型未加約束直接使用屬性 | 直接存取 T 的屬性會導致編譯錯誤,除非加上 extends。 |
為泛型加上適當的 約束 (extends)。 |
在 strictNullChecks 下忽略 undefined |
可選參數或預設值未提供時會是 undefined,若未處理會產生 runtime error。 |
在程式中檢查 undefined,或使用 非空斷言 (!) 但要小心。 |
最佳實踐
- 盡量使用具體型別:不要只寫
any,即使是臨時測試也應盡快改為正確型別。 - 函式型別抽離:對於重複使用的函式簽名,使用
type或interface讓程式更具可讀性。 - 預設值優先於可選參數:若參數有合理的預設值,直接給定,減少
undefined判斷。 - 利用
readonly:對於不會被修改的參數,使用readonly限制意外變更。 - 保持單一職責:每個函式只做一件事,這樣型別也會更簡潔、易於測試。
實際應用場景
1. API 客戶端的請求函式
在前端與後端溝通時,常需要一個通用的 request 函式,接受不同的參數與回傳不同型別的資料。
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type RequestParams = Record<string, string | number>;
interface ApiResponse<T> {
data: T;
status: number;
}
// 泛型函式:根據傳入的型別 T,回傳相對應的資料結構
async function request<T>(
url: string,
method: HttpMethod = 'GET',
params?: RequestParams,
body?: any
): Promise<ApiResponse<T>> {
const query = params
? '?' + new URLSearchParams(params as any).toString()
: '';
const response = await fetch(url + query, {
method,
headers: { 'Content-Type': 'application/json' },
body: body ? JSON.stringify(body) : undefined,
});
const data = (await response.json()) as T;
return { data, status: response.status };
}
// 呼叫範例
interface User {
id: number;
name: string;
email: string;
}
async function getUser(id: number) {
const res = await request<User>(`/api/users/${id}`);
return res.data; // data 的型別已被推斷為 User
}
2. 表單驗證函式
表單驗證通常需要接受多個欄位,且每個欄位的驗證規則不同。利用剩餘參數與泛型,可建立彈性的驗證工具。
type Validator<T> = (value: T) => string | null;
function validateAll<T>(value: T, ...validators: Validator<T>[]): string[] {
const errors: string[] = [];
for (const v of validators) {
const err = v(value);
if (err) errors.push(err);
}
return errors;
}
// 範例:字串長度驗證
const minLength = (len: number): Validator<string> => (v) =>
v.length >= len ? null : `字串長度必須至少 ${len} 個字元`;
const isEmail: Validator<string> = (v) =>
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v) ? null : '不是有效的 Email 格式';
const errors = validateAll('abc', minLength(5), isEmail);
// errors => ["字串長度必須至少 5 個字元", "不是有效的 Email 格式"]
3. 排序與比較函式
在資料表格或列表中,常需要根據不同欄位動態產生比較函式。透過函式型別別名與泛型,可輕鬆抽象化。
type Sorter<T> = (a: T, b: T) => number;
function createSorter<T, K extends keyof T>(key: K, asc = true): Sorter<T> {
return (a, b) => {
const valA = a[key];
const valB = b[key];
if (valA < valB) return asc ? -1 : 1;
if (valA > valB) return asc ? 1 : -1;
return 0;
};
}
// 使用範例
interface Product {
id: number;
name: string;
price: number;
}
const products: Product[] = [
{ id: 1, name: '筆記本', price: 1200 },
{ id: 2, name: '滑鼠', price: 350 },
{ id: 3, name: '鍵盤', price: 800 },
];
const byPriceDesc = createSorter('price', false);
const sorted = products.sort(byPriceDesc);
// sorted => 按 price 從大到小排序
總結
函式參數與回傳值的型別是 TypeScript 中最基礎、也是最核心的概念之一。透過 明確的型別註記、可選參數/預設值、剩餘參數、函式型別別名 以及 泛型,我們可以:
- 在編譯階段即捕捉大部分錯誤,減少 runtime 問題。
- 讓程式碼自我說明(self‑documenting),提升團隊協作效率。
- 建立可重用、可擴充的公共 API,降低維護成本。
在實務開發中,請養成 盡量寫出完整型別、使用泛型抽象化、遵守參數順序與最佳實踐 的習慣。如此一來,無論是小型專案還是大型企業級系統,TypeScript 都能為你的函式提供堅實的型別保護,讓開發體驗更順暢、產品更可靠。祝你在 TypeScript 的旅程中玩得開心、寫得更好!