前一篇用物件的欄位判斷兩份資料能不能互相使用。函式也能被存進變數、當成參數傳遞,因此同樣需要一份契約:呼叫端會提供哪些資料,函式完成後又會交回什麼結果。
這篇會從參數與回傳值開始,接著處理回呼函式、選填參數、剩餘參數與多載。
先替函式寫出型別:
type AnswerChecker = (answer: string) => boolean;
const checkTypeScript: AnswerChecker = (answer) => {
return answer.trim().toLowerCase() === "typescript";
};
console.log(checkTypeScript("TypeScript")); // true
函式型別的寫法是 (參數名稱: 參數型別) => 回傳型別。括號內每個參數以冒號標註型別,多個參數用逗號分隔;=> 右側標註回傳值的型別。
型別檢查只確認參數與回傳值的型別,不保證判分邏輯正確;永遠回傳 true 的函式也能符合 AnswerChecker。
上面的實作用箭頭函式,也可以改成一般函式宣告。一般函式的參數同樣用 名稱: 型別,回傳型別則寫在右括號後方的冒號:
function checkTypeScript(answer: string): boolean {
return answer.trim().toLowerCase() === "typescript";
}
const checker: AnswerChecker = checkTypeScript;

回呼函式(callback)是傳給另一段程式、由那段程式呼叫的函式。作為參數時,用函式型別指定它要接收哪些參數、回傳什麼型別:
function submitAnswer(answer: string, checker: AnswerChecker): string {
return checker(answer) ? "答對了" : "再試一次";
}
TypeScript 會檢查參數型別,但允許函式實作少宣告用不到的參數。因此,AnswerVisitor 雖然定義了 answer 和 index,只宣告 answer 的函式仍然符合這份型別;呼叫時傳入的索引會被忽略。
type AnswerVisitor = (answer: string, index: number) => void;
function visitAnswers(answers: string[], visit: AnswerVisitor): void {
answers.forEach((answer, index) => visit(answer, index));
}
// 顯示作答位置,需要答案與索引
visitAnswers(["A", "B"], (answer, index) => {
console.log(`第 ${index + 1} 筆:${answer}`);
});
// 只印出答案,不需要索引
visitAnswers(["A", "B"], (answer) => {
console.log(answer);
});
不使用回傳值 / void
函式型別中的void表示呼叫端不使用回傳值,實作仍可回傳其他值。
問號放在參數名稱後方,代表呼叫時可以不提供該參數:
function formatFeedback(message: string, label?: string): string {
if (label === undefined) {
return message;
}
return `${label}:${message}`;
}
formatFeedback("答對了");
formatFeedback("答對了", "第 3 題");
參數設為選填後,呼叫端可能不提供值,因此函式內的參數型別會包含 undefined。使用前必須先判斷是否有值,或設定預設值來處理缺值情況。
回呼函式的參數是否加 ?,決定的是呼叫時能不能省略值:
| 參數宣告 | 呼叫時的要求 | 實作中參數的型別 |
|---|---|---|
index: number |
必須傳入數字 | number |
index?: number |
可以不傳 | `number |
type OptionalIndexVisitor = (answer: string, index?: number) => void;
前面提過,函式實作本來就可以少宣告用不到的參數,不需要加 ?。只有呼叫時真的允許不傳值,才把參數設為選填;需要使用這個參數的實作,也就必須處理 undefined。
回呼函式的選填參數 / Optional parameters in callbacks
官方選填參數文件展示了把索引標成選填後,使用端會遇到的缺值錯誤,可用來對照自己的回呼函式宣告。
剩餘參數(rest parameter)寫成 ...變數名稱,會把傳入的引數收集成陣列。TypeScript 再用冒號標註陣列型別,例如 ...messages: string[]。需要把數量不固定的回饋組成一句話時,就可以這樣寫:
function joinFeedback(...messages: string[]): string {
return messages.join(";");
}
joinFeedback("答對了");
joinFeedback("答對了", "連續答對 3 題", "繼續保持");
剩餘參數可以收集零到多個引數,且必須放在參數列表最後;若前面有固定參數,則只收集它們之後的引數。
多載(function overload)讓同一個函式依不同的參數型別,對應不同的回傳型別。
前面用 type 定義函式型別。TypeScript 也允許用 function 開頭、分號結尾的宣告,直接描述某個函式的參數與回傳型別。這種宣告沒有 { ... } 函式本體,只用於型別檢查。
多載使用這種語法,先列出允許的呼叫方式,再接一份同名的函式實作:
執行時只有一個函式,各種輸入都由同一份本體處理。
// 正規化單一答案回傳字串
function normalizeAnswer(value: string): string;
// 正規化一組答案回傳陣列
function normalizeAnswer(value: string[]): string[];
function normalizeAnswer(value: string | string[]): string | string[] {
if (typeof value === "string") {
return value.trim();
}
return value.map((item) => item.trim());
}
const oneAnswer = normalizeAnswer(" A ");
// string
const manyAnswers = normalizeAnswer([" A ", " B "]);
// string[]
呼叫端只依對外的多載簽章檢查。此例若傳入尚未縮小的 string | string[] 引數,就無法符合任一多載。
多載的價值在於保留這裡的對應關係:傳入字串得到字串,傳入陣列得到陣列。若幾種輸入都回傳相同型別,直接以聯集型別標註參數通常更簡單,不需要為每種輸入重複列一份簽章。
函式多載 / Function overload
想確認哪些呼叫能符合多載,可以查閱官方多載文件,其中也比較了多載與聯集參數的適用情況。
函式契約已經能描述固定的輸入與輸出;當兩者之間還要保留同一個型別關係時,就需要在這份契約上再加入型別參數。
本文同步分享於個人部落格:https://johnsonchen.dev/blog/typescript/function-types/