Skip to content

TypeScript(2) 函數與泛型

Posted on:2024年8月9日 at 下午09:00
TypeScript(2) 函數與泛型

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,你必須明示地宣告它。

anyunknown 型別

但是,有一個需要強調的觀點:盡量不要用 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)的基礎。未來我們會有更清楚的說明。

unknownany 最大的差別是,在你為 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" },
];