物件型別搞定後,下一個天天碰的東西是函式。TS 在函式上著墨的型別細節比想像中多——可選參數、預設值、重載、this、子型別規則、回呼裡的 void——這篇一次釐清。
參數與回傳型別
function add(a: number, b: number): number {
return a + b
}
// 回傳值通常可省略,讓 TS 推導
function double(x: number) {
return x * 2 // 推導為 number
}
例外是對外暴露的 API:慣例上會顯式標 return type,免得日後改實作時不小心破壞外部契約。
可選參數與預設值
function greet(name: string, greeting?: string) {
return `${greeting ?? 'Hello'}, ${name}`
}
function greet2(name: string, greeting = 'Hello') {
return `${greeting}, ${name}` // 預設值參數自動可選,型別由預設值推導
}
可選參數要放在必填參數後面。
rest 參數
function sum(...nums: number[]): number {
return nums.reduce((a, b) => a + b, 0)
}
sum(1, 2, 3)
rest 參數也可以標成 tuple 型別:
function f(...args: [string, number, boolean]) {}
f('a', 1, true) // 必須剛好三個且型別對
函式型別的兩種寫法
// 1. type alias 式
type BinaryOp = (a: number, b: number) => number
const add: BinaryOp = (a, b) => a + b
// 2. interface call signature
interface BinaryOp2 {
(a: number, b: number): number
}
兩者等價。型別寫好後,實作時參數可以省略型別(從上下文推導):
const greet: (name: string) => string = (name) => `hi, ${name}`
// ^ 不用再寫 :string
這個叫做 contextual typing。
void 回傳的彈性
type Callback = (item: number) => void
const cb: Callback = (n) => n * 2 // ✅ 回傳值會被忽略,不會報錯
Array.prototype.forEach 的 callback 就是 void,所以可以直接傳會回傳東西的函式。
函式重載(overload)
同一個函式,不同型別的輸入要對應不同型別的回傳值——這種情況就用重載:
// 1. 寫多個簽章(重載宣告)
function pick(arr: string[], n: number): string
function pick(arr: number[], n: number): number
// 2. 寫實作簽章(對外不可見,型別必須相容所有重載)
function pick(arr: any[], n: number): any {
return arr[n]
}
const a = pick(['a', 'b'], 0) // string
const b = pick([1, 2], 0) // number
注意:
- 實作簽章的型別不會被外部看到,呼叫端只看得到上面的重載
- 多數情況用 union / 泛型就能搞定,重載多用在 union 解決不了的場景
對比可用 union 的版本:
function pickU<T>(arr: T[], n: number): T {
return arr[n]
}
this 型別
callback 裡的 this 通常透過第一個參數明示:
interface Card {
suit: string
value: number
}
interface Deck {
cards: Card[]
createCardPicker(this: Deck): () => Card
}
this: Deck 不是真實參數,呼叫時不用傳,編譯後也會消失。它只是告訴 TS:這個函式必須透過某個 Deck instance 呼叫(例如 deck.pick())。所以把方法抓出來單獨呼叫——const fn = deck.pick; fn()——會直接編譯錯誤,因為 this 綁不到 Deck。
函式型別的子類型規則
兩條重要規則,不熟會踩雷。
1. 參數逆變(contravariance)
「需要的參數比較寬,回傳比較窄」的函式才是 subtype。
type Animal = { name: string }
type Dog = Animal & { bark: () => void }
let f1: (a: Animal) => Dog = (a) => ({ name: a.name, bark: () => {} })
let f2: (d: Dog) => Animal = f1 // ❌ 嚴格模式下不行
把 strictFunctionTypes 關掉,TS 才會放寬這條檢查;至於方法簽章(method shorthand)天生就比較寬鬆——這是歷史包袱。
2. 參數可少不可多
實作端可以宣告得比簽章少幾個參數(多出來的引數在執行期被忽略),但呼叫端還是要照簽章傳:
type F = (a: number, b: number) => void
const f: F = (a) => console.log(a) // ✅ 少實作一個參數可以
f(1, 2) // 呼叫端必須照簽章來
泛型函式(簡介)
function identity<T>(x: T): T {
return x
}
identity<string>('hi') // 顯式
identity('hi') // 推導出 T = string
更實用的例子:
function firstOrNull<T>(arr: T[]): T | null {
return arr.length > 0 ? arr[0] : null
}
const n = firstOrNull([1, 2, 3]) // T 推導為 number, 回傳 number | null
const s = firstOrNull(['a']) // T 推導為 string
TS 從參數型別推導 T,不用手寫 firstOrNull<number>(...)。
更深入的泛型放下一篇。
arrow function vs function declaration
兩者在 TS 裡型別行為相同,差別跟 JS 一樣(this 綁定、hoisting)。
const f = (x: number): number => x * 2
function g(x: number): number {
return x * 2
}
常見陷阱
1. 推導不出 this
const obj = {
count: 0,
inc: () => this.count++, // ❌ arrow 沒有 this
}
對物件方法用 shorthand:
const obj = {
count: 0,
inc() { this.count++ }, // ✅
}
2. 物件字面值傳函式參數的多餘屬性檢查
function f(opts: { name: string }) {}
f({ name: 'a', extra: 1 }) // ❌ extra 多餘
抽成變數可繞過(前一篇提過)。
3. 解構參數標型別的位置
function f({ a, b }: { a: number; b: string }) {}
// ^ 型別標在解構後面
不能寫成 function f({ a: number, b: string }),那會變成「把 a 重新命名為 number」。
上面那個 identity<T> 已經摸到泛型的門口,下一篇就把泛型完整拆開講。
Latest Updates
- 2026.06.11 Content updated
