學習資源
函式的型別
在 JavaScript 中,有兩種常見的定義函式的方式——函式宣告(Function Declaration)和函式表達式(Function Expression):
// 函式宣告(Function Declaration)
function add(a, b) {
return a + b;
}
// 函式表達式(Function Expression)
const addNumbers = function (x, y) {
return x + y;
};
在 TypeScript 中,函式的型別透過參數型別(輸入)的返回值型別(輸出)來定義,以下會分別說明函式宣告和函式表達式如何定義其函示的型別。
函式宣告
一樣沿用上面的例子,在 TypeScript 中會這樣定義函式宣告的型別:
function add(a: number, b: number): number {
return a + b;
}
使用函式時需注意,傳遞給函式的引數不能少於或多於函式定義的參數數量:
function add(a: number, b: number): number {
return a + b;
}
// 🔴 這會報錯,因為只傳入了一個引數,但函式需要兩個引數
add(1);
// 🔴 這也會報錯,因為傳入了三個引數,但函式只需要兩個引數
add(1, 2, 3);
函式表達式
一樣沿用上面的例子,在 TypeScript 中我們第一直覺可能會這樣定義函式表達式的型別:
const addNumbers = function (x: number, y: number): number {
return x + y;
};
這樣寫雖然不會報錯,但實際上這樣只有定義等號 =
右側的匿名函式 function (x, y) {}
的型別,而左側變數 addNumbers
是 TypeScript 根據右側的匿名函式的型別來自動推導出它的型別。
如果我們想要明確指定變數 addNumbers
的型別,需要在 addNumbers
後方使用冒號 :
來指定它的型別為一個函式:
const addNumbers: (x: number, y: number) => number = function (
x: number,
y: number
): number {
return x + y;
};
在 TypeScript 的型別定義中,箭頭(=>
)用於表示函式的定義。箭頭左邊表示函式的參數型別,箭頭右邊表示函式的返回值型別。
注意不要跟 ES6 中表示箭頭函示(Arrow Function)的箭頭(=>
)搞混了。
用介面定義函式的型別
也可以使用介面來定義函式的型別,將參數和返回值的型別指定在介面中,如下所示:
// 使用介面定義函式型別
interface AddFunction {
(a: number, b: number): number;
}
// 使用定義的介面來指定函式的型別
const addNumbers: AddFunction = function (a: number, b: number): number {
return a + b;
};
console.log(addNumbers(1, 2)); // 輸出 3
可選參數
與介面的可選屬性類似,在參數名的後面加上 ?
符號來標記可選參數:
function greet(name: string, age?: number): string {
if (age) {
return `Hello, my name is ${name} and I am ${age} years old.`;
} else {
return `Hello, my name is ${name}.`;
}
}
console.log(greet("Alice")); // "Hello, my name is Alice."
console.log(greet("Bob", 30)); // "Hello, my name is Bob and I am 30 years old."
使用可選參數有幾個注意事項:
可選參數必須放在必須參數之後
在函數的參數列表中,可選參數應該放在必需參數之後。這是因為 TypeScript 是根據參數的順序來確定是否傳遞了可選參數。例如:
// 錯誤示範:可選引數 age 放在了必需引數 name 之前
function greet(age?: number, name: string): string {
// ...
}
設定預設值時要注意可選參數
當我們為可選參數設定了預設值時,無需再使用 ?
標記,因為預設值已經表示了參數是可選的,也不會受到「可選參數應該放在必需參數之後」的限制了,想放哪就放哪。例如:
// 正確示範:age 有預設值,無需使用 '?'
function greet(name: string, age: number = 0): string {
// ...
}
使用可選參數時要注意未定義值
因為可選參數可能未傳入值且沒有預設值,所以在函數內部使用可選參數時,要注意檢查它是否為 undefined
以免出現非預期的情況。例如:
function greet(name: string, age?: number): string {
if (age === undefined) {
// 如果沒有加判斷,未傳入 age 時會返回 I am undefined years old.
return `Hello, my name is ${name}.`;
} else {
return `Hello, my name is ${name} and I am ${age} years old.`;
}
}
其餘參數
當我們想處理數量不固定的參數時,與 ES6 的其餘參數(Rest Parameter)一樣,將最後一個參數以 ...
開頭,它會將其餘的參數視為一個陣列:
function sum(...numbers: number[]): number {
return numbers.reduce((acc, curr) => acc + curr, 0);
}
console.log(sum(1, 2)); // 3
console.log(sum(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)); // 55
函式多載(Overloads)
函式多載的意思是同一個函式名稱定義多個型別。根據傳入的引數類型和數量,TypeScript 編譯器會自動選擇合適的函數型別。可以讓我們為不同的引數組合提供各別的型別檢查,提高程式碼的可讀性和維護性。
讀到這邊可能會想,用聯合型別不是也能定義多個型別嗎?但函式跟一般變數不同處是它會有返回值,當不同型別的輸入有不同的型別的輸出,這時用函式多載來定義會更容易理解。
一樣先上例子:
function process(input: string | number): string[] | number {
if (typeof input === 'string') {
return input.split('');
} else {
return input * 2;
}
}
let result1 = process("hello"); // 結果: ["h", "e", "l", "l", "o"]
let result2 = process(10); // 結果: 20
在這個例子中,當傳入的 input
是 string
類型的引數,process
函式會回傳 string[]
類型的值;當傳入的 input
是 number
類型的引數,process
函式會回傳 number
類型的值。
但這是當我們把函式的實作內容全看過一遍才會知道的事,只看 function process(input: string | number): string[] | number
不會確定是輸入 string
回傳 string[]
、number
回傳 number
,一但實作的內容更複雜,想用 any
的衝動肯定都湧上來了(誤)。
而函式多載就來解決這個問題的:
// 函式型別一:接受 string 類型的引數,返回 string[]
function process(input: string): string[];
// 函式型別二:接受 number 類型的引數,返回 number
function process(input: number): number;
// 原本的函式
function process(input: string | number): string[] | number {
if (typeof input === 'string') {
return input.split('');
} else {
return input * 2;
}
}
let result1 = process("hello"); // 結果: ["h", "e", "l", "l", "o"]
let result2 = process(10); // 結果: 20
對比有無使用函式多載在編譯器的程式碼提示有什麼不同:
沒有使用函式多載
使用函式多載
另外,由於 TypeScript 在決定使用哪個多載定義時,會從最前面的多載定義開始,如果找到一個適合的定義,就會停止尋找。因此當我們有多個多載定義時,要把型別定義範圍較小較精確的放在前面,確保 TypeScript 編譯器選擇正確的定義。
型別斷言(Type Assertion)
型別斷言是 TypeScript 中的一個語法特性,它允許我們指定更準確的型別,提供 TypeScript 編譯器無法自行推斷的額外型別資訊。
例如,我們使用 document.getElementById
來選取頁面上的元素,TypeScript 只能推斷出這會返回某種 HTMLElement
,不知道究竟是哪一種,但我們可能知道選中的會是一個 HTMLCanvasElement
類型的元素
。
在這個情況下,我們就能使用型別斷言來指定更具體的型別,在 TypeScript 中,有兩種寫法來指定型別斷言:
尖括號
<>
(angle-bracket)寫法:const myCanvas = <HTMLCanvasElement>document.getElementById("main_canvas");
as
寫法:const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;
在 TypeScript 的 TSX 文件中,尖括號 <T>
語法與 JSX 語法的標籤(例如 <div>
)有語法衝突。因為尖括號在 JSX 語法中用來表示一個標籤的開始,TypeScript 無法分辨尖括號是用於 JSX 元素還是型別斷言亦或是泛型。為了避免這種衝突,TypeScript 引入了 as
語法作為 TSX 文件中的替代方法。
所以在 TSX 文件中,我們應該使用 as
語法進行型別斷言,而不是尖括號 <T>
語法。
接下來介紹一些需要使用型別斷言的常見情況。
處理任意值 any
型別
當我們在處理一個 any
型別的值時,可能需要將其斷言為更具體的型別。這樣可以避免在後續的程式碼中使用額外的型別檢查和警告。
let value: any = "This is a string.";
// 沒有使用型別斷言
let stringLength1: number = value.length; // 編譯器不會報錯,但無法確保 `value` 真的是字串
// 使用型別斷言
let stringLength2: number = (value as string).length; // 確保編譯器將 `value` 視為字串,並且能夠取得其長度
處理聯合型別
在使用聯合型別時,某些情況下 TypeScript 編譯器可能無法確定目標值的確切型別。在這種情況下,我們可以使用型別斷言來明確指定目標值的型別。
interface AnimalWithName {
name: string;
}
interface AnimalWithAge {
age: number;
}
let myAnimal: AnimalWithName | AnimalWithAge = { name: "Dog" };
// 沒有使用型別斷言
let animalName1: string = myAnimal.name;
// 因為 `myAnimal` 型別有可能是 `AnimalWithAge`,但 `AnimalWithAge` 並沒有 `name` 屬性
// 所以編譯器會報錯:Property 'name' does not exist on type 'AnimalWithName | AnimalWithAge'
// 使用型別斷言
let animalName2: string = (myAnimal as AnimalWithName).name; // 確保編譯器將 `myAnimal` 視為具有 `name` 屬性的物件,並且能夠獲取其值
處理不同 API 返回的數據
當使用外部 API 或第三方套件時,有時候返回的數據可能包含不同的型別。在這種情況下,我們可以使用型別斷言來確保程式碼使用正確的型別。
interface ApiResponse {
status: "success" | "error";
data?: any;
message?: string;
}
function handleApiResponse(response: ApiResponse) {
if (response.status === "success") {
// 沒有使用型別斷言
let data1 = response.data; // `data1` 的型別為 `any`,編譯器無法提供有關其屬性或方法的提示
// 使用型別斷言
let data2 = response.data as { name: string, age: number }; // 確保編譯器將 `response.data` 視為具有 `name` 和 `age` 屬性的物件
} else {
console.error(response.message);
}
}
要注意使用型別斷言的同時也要謹慎,因為它會跳過 TypeScript 的型別檢查。如果斷言不正確,可能會導致意外的行為和錯誤。因此在使用型別斷言時,應確定自己對目標值的型別有足夠的了解。