[TypeScript] 函式

函式參數

以下函式:

function sing(song){
  console.log(`Singing: ${song}!`);
}

song 參數因為沒有明確宣告型別資訊,所以 TypeScript 會認為它是 any 型別:表示參數的型別可以是任何型別。

若要告知 song 為 string 型別的話,可用以下寫法:

function sing(song: string){
  console.log(`Singing: ${song}!`);
}

必要參數

TypeScript 在預設上,是假定所有參數都是必要的。如果使用錯誤數量的參數呼叫函式,TypeScript 將以型別錯誤的形式提出訊息。

例:

function singTwo(first: string, second: string) {
  console.log(`${first} / ${second}`);
}

// 錯誤:應有 2 個引數,但只得到 1 個。
singTwo("Hi");

// 正確
singTwo("Hi", "Hello");

// 錯誤:應有 2 個引數,但卻得到 3 個。
singTwo("Hi", "Hello", "Wow");

註:參數(Parameter) 是指函式宣告所期望接收的參數。引數(Argument)是指函式在呼叫時,提供給參數的資料,例如上面程式碼中,first 和 second 是參數,而 “Hi”、”Hello” 之類的是引數。

選項參數

在 JavaScript 中,如果未提供函式參數,則函式內部的參數值,預設就是 undefined。

然而,在 TypeScript 中,可透過加上問號,來讓參數變成是可選擇的。

例如以下程式,singer 參數被標記為可選擇的,它的型別是 string | undefined

function announceSong(song: string, singer?: string){
  console.log(`Song: ${song}`);

  if(singer){ // 將 singer 窄化為 string
    console.log(`Singer: ${singer}`);
  }
}

announceSong("Hi"); // 允許
announceSong("Hi", undefined); // 允許
announceSong("Hi", "Sia"); // 允許

如果是改成用以下的寫法,那麼 singer 參數就是一定要提供:

function announceSongBy(song: string, singer: string | undefined){
  console.log(`Song: ${song}`);

  if(singer){ // 將 singer 窄化為 string
    console.log(`Singer: ${singer}`);
  }
}

announceSongBy("Hi"); // 錯誤:應有2個引數,但只有1個。
announceSongBy("Hi", undefined); // 允許
announceSongBy("Hi", "Sia"); // 允許

註:函式當中的選擇性參數,必須擺在最後一個位置。倘若放在必要參數之前,那就會報錯。例:

// 以下參數會報錯,必要參數不得放在選擇性參數之後。
function announceSinger(singer?: string, song: string){
}

參數的預設值

可以使用等號來指派參數的預設值。

函式參數的預設值,若沒有給型別的話,TypeScript 將根據預設值推斷參數的型別:

// rating 參數的型別會推斷為 number | undefined
function rateSong(song: string, rating = 0){
    console.log(`${song} 獲得了 ${rating} 顆星!`);
}

rateSong("Photograph"); // 允許,會印出「Photograph 獲得了 0 顆星!」
rateSong("hi1", 5); // 允許,會印出「hi1 獲得了 5 顆星!」
rateSong("hi2", undefined); // 允許,會印出「hi2 獲得了 0 顆星!」

rateSong("hi3", "100"); // 報錯,型別 string 的引數不可指派給型別 number 的參數。

剩餘參數

TypeScript 允許宣告剩餘(Rest)的型別,需要在結尾處增加 [] 語法,用以表示它是陣列的參數

下例中,songs 為函式 singAllTheSongs 剩餘參數,並且是 0 個或多個 string 型別的陣列參數:

function singAllTheSongs(singer: string, ...songs: string[]){
    for(const song of songs){
        console.log(`${singer} 唱 ${song}。`);
    }
}

singAllTheSongs("Alice"); // 允許
singAllTheSongs("Mary", "a", "b"); // 允許

singAllTheSongs("Mary", 100); // 報錯,型別 number 的引數不可指派給型別 string 的參數。

回傳型別

TypeScript 會自動依據函式的回傳值,來自動推斷該函式的回傳型別,

例 1:

// 型別:(songs: string[]) => number
function singSongs(songs: string[]){
  for(const song of songs){
    console.log(`${song}`);
  }
  return songs.length;
}

例 2:

// 型別:(songs: string[], index: number) => string | undefined
function getSongAt(songs: string[], index: number){
  return index < songs.length ? songs[index] : undefined;
}

明確的回傳型別

與變數一樣,通常不建議以明確宣告方式,在函式的回傳型別中使用型別註記。

一般函式(下方的藍色文字即明確宣告該函式的回傳型別):

function abc(): number{
  return 1;
}

如果是箭頭函式(下方的藍色文字即明確宣告該函式的回傳型別):

const abc = (): number => {
  return 1;
};

函式型別

函式型別語法看起來類似於箭頭函式,但操控的是型別而非主體。

例1:

// 描述一個沒有參數,並且回傳 string 數值的函式
let nothingInGiveString: () => string;

例2:

// 描述一個帶有 string[] 參數、一個可選用的 count 參數,和一個回傳 number 的函式
let inputAndOutput: (songs: string[], count?: number) => number;

另外,函式型別也常用於描述回呼(callback)參數,例3:

const songs = ["a", "b", "c"];

function runOnSongs(getSongAt: (index: number) => string) {
  for(let i = 0; i < songs.length; i++){
    console.log(getSongAt(i));
  }
}

function getSongAt(index: number){
  return "" + songs[index] + "";
}

runOnSongs(getSongAt); // 允許

function logSong(song: string){
  return "" + song + "";
}

runOnSongs(logSong); // 報錯:型別 (song: string) => string 引數,不可指派給型別 (index: number) => string 參數。參數 song 和 index 的型別不相容,型別 string 不可指派給型別 number。

函式型別使用小括號

在聯集型別中,小括號可用於標示哪一部份,是函式回傳的聯集型別:

// 一個回傳型別為 string | undefined 聯集的函式
let returnsStringOrUndefined: () => string | undefined;

// 可能為 undefined 或回傳型別為 string 的函式
let maybeReturnsString: (() => string) | undefined;

參數型別推斷

TypeScript 可以推斷函式中的參數型別,以及提供給相關位置的宣告型別。

例如以下:

let ginger: (song: string) => string;

// 以下的 song 型別推斷為 string
singer = function(song){
  return "Singing: " + song.toUpperCase();
}

再例如以下:

const songs = ["a", "b", "c"];

// song 推斷為 string
// index 推斷為 number
songs.forEach((song, index) => {
  console.log(song + " is at index " + index);
});

函式型別別名

例:

// 這個 StringToNumber 是一個函式型別別名
type StringToNumber = (input: string) => number;

let stringToNumber: StringToNumber;

stringToNumber = (input) => input.length; // 正確

let stringToNumber2: StringToNumber;
stringToNumber2 = (input) => input.toUpperCase(); // 會報錯:型別 string 不可指派給型別 number

函式型別別名也可以用在函式的參數。例:

// 這個 NumberToString 是一個函式型別別名
type NumberToString = (input: number) => string;

function usesNumberToString(numberToString: NumberToString) {
  console.log("The string is: " + numberToString(1234));
}

usesNumberToString((input) => {
  return input + "! Hooray!";
}); // 正確

// 以下錯誤:型別 number 不可指派給型別 string
usesNumberToString((input) => input * 2);

其它回傳型別

回傳 void 型別

有些函式,並沒有任何回傳值。可能是沒有任何 return 語句,或者也有可能是 return 語句上沒有回傳資料。TypeScript 允許使用 void 關鍵字,來表達這種情況。

回傳型別為 void 的函式,可能不會回傳值。因此下例的 logSong 函式,宣告為回傳 void,因此不允許回傳資料:

function logSong(song: string | undefined): void {
  if(!song){
    return; // 正確
  }

  console.log("" + song + "");

  return true; // 錯誤:型別 boolean 不可指派給型別 void
}

void 可用在函式型別宣告中的回傳型別。在函式型別宣告中使用時,void 表示函式的任何回傳值都會被忽略。例如以下範例:

// songLogger 變數會是一個函式,它接收一個字串,且不回傳資料
let songLogger: (song: string) => void;

songLogger = (song) => {
  console.log("" + song + "");
};

songLogger("Hello"); // 正確

注意:雖然 JavaScript 函式在沒有實際回傳資料的情況下,預設是回傳 undefined ,但 void 與 undefined 是不一樣的。void 表示函式的回傳型別將被忽略,而 undefined 是回傳的字面數值。嘗試將型別為 void 的值指派給型別包含 undefined 的值,會造成型別錯誤

// returnsVoid 的回傳型別會推斷為 void
function returnsVoid(){
  return;
}
let lazyValue: string | undefined;

// 以下錯誤:型別 void 不可指派給型別 string | undefined
lazyValue = returnsVoid();

最後留意:void 型別在 JavaScript 中並沒有,它是 TypeScript 中的關鍵字,用於宣告函式的回傳型別。需記得,void 並不是表示函式本身可以回傳的數值,而是回傳值並不打算使用。

回傳 never 型別

有些函式不僅不回傳資料,反而是刻意不回傳。永遠不回傳的函式,是那些總是拋出錯誤或執行無限循環的函式。

如果函式刻意永遠不回傳資料,那就需明確加入 never 型別註記,表示執行該函式後的任何程式碼都不會執行

例:

function fail(message: string): never {
  throw new Error("錯誤:" + message);
}

function workWithUnsafeParam(param: unknown) {
  if(typeof param !== "string"){
    fail("參數應是 string。");
  }

  // 這裡的 param 已知是 string 型別
  param.toUpperCase(); // 正確
}

註:never 不等於 void。void 用於不回傳任何資料的函式;never 用於表示永遠不會回傳的函式。

函式重載

某些 JavaScript 函式可以使用完全不同的參數集合來呼叫,這些參數集合會以可選擇的參數或剩餘參數來表示。

這些函式在 TypeScript 語法,可稱為 重載特徵/重載簽章(overload signatures) 來描述:在一個最終實做特徵(implementation signature)的函式主體之前,先宣告多個不同版本的函式名稱、參數和回傳型別。

下例:前兩行是重載特徵,第三行是實作特徵:

function createDate(timestamp: number): Date;
function createDate(month: number, day: number, year: number): Date;
function createDate(monthOrTimestamp: number, day?: number, year?: number) {
  return day === undefined || year === undefined ? new Date(monthOrTimestamp) : new Date(year, monthOrTimestamp, day);
}

createDate(554356800); // 正確
createDate(7, 28, 1987); // 正確

// 報錯:沒有任何重載預期 2 個引數,但有重載預期1或3個引數
createDate(4, 1);

註:函式重載通常用作複雜,用來表示難以敘述的函式型別之最後手段。一般來說,最好還是保持函式的簡單,並且盡可能避免使用函式重載。

留言

發佈留言