
在 JavaScript
中,有幾種常見的定義函式的方法,包括函式宣告(Function Declaration)、函式表達式(Function Expression)和箭頭函式(Arrow Function):
// 函式宣告,最傳統的函式宣告方式
function hello(name) {
return `Hello, ${name}`;
}
// 函式表達式,將一個匿名函式指派給一個變數
const hello = function (name) {
return `Hello, ${name}`;
};
// 箭頭函式,更簡潔的函式表達式的寫法,在 ES6 中引入
const hello = name => `Hello, ${name}`;
使用 TypeScript
,可以直接定義參數和 return
的型別:
function hello(name: string): string {
return `Hello, ${name}`;
}
const hello = function (name: string): string {
return `Hello, ${name}`;
};
const hello = (name: string): string => `Hello, ${name}`;
除了直接在函式宣告時定義參數和返回值的型別,我們還可以把函式的型別獨立定義,只是這時,只能使用函式表達式或是箭頭函式:
type MathFunction = (x: number, y: number) => number;
const add: MathFunction = (x, y) => x + y;
// 或使用函式表達式:
const add: MathFunction = function (x, y) {
return x + y;
};
TypeScript
會自動判斷出 return
的型別,因此,在合適條件下,我們可以省略 return
型別的宣告:
function hello(name: string) {
return `Hello, ${name}`;
}
如果沒有返回值,除了省略 return
型別的宣告外,也可以使用 void
型別來明確宣告:
function hello(name: string): void {
console.log(`Hello, ${name}`);
}
然而,傳入的參數的型別不該省略,在嚴格模式下,TypeScript
會出現錯誤:
function hello(name) {
return `Hello, ${name}`;
}
// Parameter 'name' implicitly has an 'any' type.
這個錯誤訊息表示,這裡的 name
因為並未指派型別,並且無法從宣告中判斷其型別,因此 TypeScript
只好將它當成 any
型別,意思是它可能是任何型別,這會使得 TypeScript
幾乎對它不會做任何檢查。而在嚴格模式下。「自動判斷為 any
」是不能被允許的。
如果你真的希望資料的型別是 any
,你必須明示地宣告它。
any
與 unknown
型別
但是,有一個需要強調的觀點:盡量不要用 any
型別,除非非不得已。
any
型別差不多是告訴 TypeScript
:「你不要管這個變數的型別,我會自己判斷」。TypeScript
幾乎不會對 any
型別做任何檢查,使得所有的 TypeScript
錯誤都被掩蓋起來。
大部分需要使用 any
的情境,我們可以使用 unknown
型別來做到這件事。在下面的例子中,我們不清楚 data
的型別,因此我們將它指派為 unknown
:
function processUserData(data: unknown): { name: string; age: number } {
if (typeof data === "object" && data !== null) {
// 到了這一行,TypeScript 知道 data 是一個物件,並且不是 null。
if ("name" in data && "age" in data) {
// 到了這一行,TypeScript 知道 data 有 name 和 age 屬性,但不知道它們的類型。
if (typeof data.name === "string" && typeof data.age === "number") {
// 到了這一行,TypeScript 知道 data.name 是字串,並且 data.age 是數字。
return { name: data.name, age: data.age };
}
}
}
throw new Error("無效的用戶資料");
}
注意上面的例子中的註解,這顯示 TypeScript
有能力根據條件來明白,在這個脈絡下的變數是或不是什麼型別,這觀念是型別防衛(type guard)的基礎。未來我們會有更清楚的說明。
unknown
和 any
最大的差別是,在你為 unknown
的變數確定型別以前,TypeScript
會禁止你對它進行先入為主的判斷,這使得你的程式碼更加健全。譬如:
function processDataUnknown(data: unknown) {
console.log(data.length);
// 錯誤:'data is of type 'unknown'.'
data.toUpperCase();
// 錯誤:'data is of type 'unknown'.'
// 我們必須先為 data 確定型別,也就是對它進行型別防衛。
if (typeof data === "string") {
console.log(data.length);
console.log(data.toUpperCase());
} else if (Array.isArray(data)) {
console.log(data.length);
} else {
console.log("data 不是字串也不是陣列");
}
}
函式參數數量限制
在 TypeScript
中,對函式的參數數量有嚴格限制,而這個限制是 JavaScript
所沒有的。
在 JavaScript
中,下列的函式宣告與呼叫是完全合法的。數量不足的參數會被當成 undefined
,數量超過的參數則會直接被忽略:
function greet(name, age) {
console.log(`Hello, ${name}! You are ${age} years old.`);
}
greet("Alice", 30);
// "Hello, Alice! You are 30 years old."
greet("Bob");
// "Hello, Bob! You are undefined years old."
greet("Charlie", 25, "extra");
// "Hello, Charlie! You are 25 years old."
但在 TypeScript
中則非如此:
function greet(name: string, age: number) {
console.log(`Hello, ${name}! You are ${age} years old.`);
}
greet("Alice", 30);
// 這是正確的呼叫。
greet("Bob");
// 錯誤: Expected 2 arguments, but got 1.
greet("Charlie", 25, "extra");
// 錯誤: Expected 2 arguments, but got 3.
如果你希望參數是可選的,必須加上 ?
或是指定預設值:
function greet(name: string, age?: number) {
console.log(`Hello, ${name}! You are ${age ?? "unknown"} years old.`);
// 這個 `??` 是 ES11 中擴增的語法,
// age ?? 'unknown' 在 age 是 null 或 undefined 的時候為 age,否則為 'unknown'
}
greet("Alice", 30);
// "Hello, Alice! You are 30 years old."
greet("Bob");
// "Hello, Bob! You are unknown years old."
function greet(name: string, age: number = 18) {
// 此處的 age 不能加上 ?,但它會被視為可選的
console.log(`Hello, ${name}! You are ${age} years old.`);
}
greet("Bob");
// "Hello, Bob! You are 18 years old."
如果想讓函數接受不定數量的參數,可以使用剩餘參數(rest parameter):
function sum(...numbers: number[]): number {
return numbers.reduce((total, num) => total + num, 0);
}
sum(1, 2);
sum(1, 2, 3, 4, 5);
sum();
函數與泛型
泛型(generic type)是 TypeScript
中的強大功能,需要靈活應用型別,就必須要學習泛型的使用。
假設我們的需求是這樣,我想定義一個名為 swap
的函式,它收一個 2 元組的同型別陣列,並將其內容交換,也就是說,它會把 [a
, b
] 變成 [b
, a
]。但它也有可能收 [1, 2]
,這樣的話它會返回 [2, 1]
。你可能會想寫成:
function swap(arr: [any, any]): [any, any] {
return [arr[1], arr[0]];
}
這會有什麼問題?你會發現這個函式不符合我們的需求,因為它可以收不同型別的陣列:
swap([1, "hello"]);
// 這不會報錯,但我們希望這要報錯!
這樣我們該怎麼寫呢?這時候我們就該使用泛型,語法如下。如果你先把 <T>
放在正確的位置上,TypeScript
會知道要根據呼叫時傳入的參數的型別來判斷 T
是什麼:
function swap<T>(arr: [T, T]): [T, T] {
return [arr[1], arr[0]];
}
swap([1, 2]);
// 者時候 T 被判斷成 'number'。
swap(["hello", "world"]);
// 這時候 T 被判斷成 'string'。
swap([1, "hello"]);
// Type 'string' is not assignable to type 'number'.
// 成功報錯了。
箭頭函式和函式表達式當然也可以使用泛型,注意 <T>
放的位置略為不同:
const swap = <T>(arr: [T, T]): [T, T] => [arr[1], arr[0]];
const swap = function <T>(arr: [T, T]): [T, T] {
return [arr[1], arr[0]];
};
這個 T
的命名是常見慣例,但並不是強硬的,你可以使用任何的非保留字作為泛型的名稱。雖然沒有真正的限制,但通常會使用大寫開頭的變數或是單一字元。
泛型變數的個數也可以有一個以上:
function merge<T, U>(obj1: T, obj2: U) {
return { ...obj1, ...obj2 };
}
// 在這個例子中,我刻意省略了 return 的型別宣告(因為它會正確推斷),但你也可以明示它:T & U。
泛型一樣可以使用獨立宣告的型別。在下面的例子裡面,我們做了一個新的示範,我們並非讓 TypeScript
透過傳入的參數來推斷泛型中的 T
,而是主動傳入型別來告知 T
是「字串或者數字」:
type GenericFunction<T> = (x: T, y: T) => T;
const genericAdd: GenericFunction<number | string> = (x, y) => {
if (typeof x === "number" && typeof y === "number") {
return x + y;
} else {
return x + y;
}
};
如果使用非同步函式,可以這樣寫:
async function fetchData<T>(url: string): Promise<T> {
return fetch(url).then(response => response.json());
}
interface User {
id: number;
name: string;
}
fetchData<User>("/api/user").then(user => {
console.log(user.name);
});
在其他型別使用泛型
以下是一些在非函數型別上使用泛型的簡單例子:
interface Box<T> {
content: T;
}
const numberBox: Box<number> = { content: 42 };
const stringBox: Box<string> = { content: "Hello" };
type DataArray<T> = T[];
const dataArray: DataArray<number> = [1, 2, 3];
interface User {
id: number;
name: string;
}
const userArray: DataArray<User> = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
];