[TypeScript] 陣列

TypeScript 遵守每個陣列保持單一資料型別,最佳作法是記住陣列中最初的資料型別,並且只允許陣列對此種數據資料進行操作。

陣列型別

例如以下:

// arrayOfNumbers 是一個存「數值」資料的「陣列」。
let arrayOfNumbers: number[];

arrayOfNumbers = [4, 8, 15];

註:陣列型別也可以使用類似 Array<number> 的語法來撰寫,稱為泛型類別(class generics),大多數開發人員偏好更簡易的 number[] 語法。

陣列與函數型別

例如以下:

// createStrings 變數是一個函式,會回傳資料為字串的陣列
let createStrings: () => string[];

// stringCreators 變數會是一個陣列,陣列當中的每個資料,會是一個回傳字串的函式
let stringCreators: (() => string)[];

聯集的陣列型別

可以使用聯集型別來表示陣列中每個元素是不同的型別。仍需要使用中括號來註記陣列的內容部分與聯集型別。

例如以下:

// 型別是 number 或 string 的陣列
let stringOrArrayOfNumbers: string | number[];

// 型別是一個陣列,陣列裡的資料會是 number 或 string
let arrayOfStringOrNumbers: (string | number)[];

另外,從陣列中的資料,也可推斷所有可能型別的聯集,例:

// 型別為 (string | undefined)[]
const namesMaybe = ["aaa", "bbb", undefined];

演變成 any 的陣列

如果沒有在最初空陣列上,為變數設定所包含的型別註記,TypeScript 會將陣列視為演變的 any[],這代表可以接收任何內容,我們並不喜歡 any[] 陣列因為這會允許增加不確定的資料,進而否定了 TypeScript 型別檢查的優點。例如:

// 此時型別為 any[]
let values = [];

// 此時型別變為 string[]
values.push("");

// 此時型別再變為 (number | string)[]
values[0] = 0;

多維陣列

二維陣列或陣列的陣列,將有兩個中括號([]) 符號:

let arrayOfArraysOfNumbers: number[][];
// 上面這行,也可以寫這樣:
let arrayOfArraysOfNumbers: (number[])[];

arrayOfArraysOfNumbers = [
  [1, 2, 3],
  [2, 4, 6],
  [3, 6, 9]
];

一個三維度陣列,將具有三個中括號([]),以此類推。

陣列成員

第一個例子:檢索陣列成員並回傳該陣列型別的元素,以下 defenders 陣列的型別是 string[],所以 defender 是一個字串:

// defenders 的型別是 string[]
const defenders = ["a", "b"];

// defender 的型別是 string
const defender = defenders[0];

第二個例子:聯集型別陣列的成員,本身就是相同的型別:

// abc 的型別為 (string | Date)[]
const abc = ["a", new Date(1999, 1, 2)];

// a 的型別會是 Date | string
const a = abc[0];

展開和剩餘

展開

陣列可以使用 … 展開運算符號。TypeScript 分析陣列結果,將可以包含來自任何陣列輸入的資料。

例如以下:已知聯集陣列包含 string 型別和 number 型別的資料,因此推斷其型別為 (string | number)[]:

// 型別為 string[]
const soldiers = ["a", "b", "c"];

// 型別為 number[]
const soldierAges = [1, 2, 3];

// 型別為 (string | number)[]
const conjoined = [...soldiers, ...soldierAges];

剩餘參數

例如以下: …names 參數只接受 string 資料的陣列。

// names 的型別為 string[]
function logWarriors(greeting: string, ...names: string[]){
  for(const name of names){
    console.log(greeting + ", " + name + "!");
  }
}

const warriors = ["a", "b", "c"];
logWarriors("Hello", ...warriors); // 允許

const birthYears = [1, 2, 3];
// 以下會報錯:型別 number 的引數不可指派給型別 string 的參數
logWarriors("Born in", ...bithYears);

元組

理論上,JavaScript 陣列可以是任意大小,但是,若可以使用固定大小的陣列,有時會較好,所以這種固定大小的陣列,就稱為元組(tuple)

元組陣列在每個索引位置都有一個特定的已知型別,它可能比陣列所有可能成員的聯集型別更加具體化。

宣告元組型別的語法,看起來像一個陣列字面值,但使用型別代替了元素值。

例:

// 宣告為一個元組型別,索引值為 0 是 number,索引值為 1 是 string
let yearAndWarrior: [number, string];

yearAndWarrior = [50, "a"]; // 允許

// 以下會報錯,型別 boolean 不可指派給型別 number
yearAndWarrior = [false, "b"];

// 以下會報錯,型別 [number] 不可指派給型別 [number, string],來源有一個元素,但目標需要 2 個。
yearAndWarrior = [530];

元組通常與 JavaScript 中的解構一起使用,才能夠一次指派多個值,例:

// year 型別會推斷是 number
// warrior 型別會推斷是 string
let [year, warrior] = Math.random() > 0.5 ? [340, "a"] : [123, "b"];

元組的指派性

元組型別被 TypeScript 視為比可變長度陣列,更為具體化的型別。這表示可變長度陣列型別,不能指派給元組型別

以下例子:

// 型別會推斷為 (boolean | number)[]
const pairData = [false, 123];

// 以下會報錯:型別 (boolean | number)[] 不可指派給型別 [boolean, number],目標需要2個元素,但來源的元素可能較少。
const pairTupleLoose: [boolean, number] = pairData;

再看下一個例子:

const tupleThree: [boolean, number, string] = [false, 123, "a"];

const tupleTwoExact: [boolean, number] = [tupleThree[0], tupleThree[1]]; // 允許

// 以下會報錯:型別 [boolean, number, string] 不可指派給型別 [boolean, number]
const tupleTwoExtra: [boolean, number] = tupleThree;

元組視為剩餘參數

因為元組具有長度資訊元素型別,被視為更豐富且具體化型別資訊的陣列,所以對於要傳遞給函式的參數特別有用。

例:

function logPair(name: string, value: number) {
  console.log(name + " has " + value);
}

const pairArray = ["a", 1];
// 會報錯:擴展引數必須具有元組型別或傳遞給剩餘參數
logPair(...pairArray);

const pairTupleIncorrect: [number, string] = [1, "b"];
// 會報錯:型別 number 的引數不可指派給型別 string 參數
logPair(...pairTupleIncorrect);

const pairTupleCorrect: [string, number] = ["c", 1];
logPair(...pairTupleCorrect); // 正確

另外一個例子,若想使用剩餘參數元組,可與陣列混合。例:

function logTrio(name: string, value: [number, boolean]) {
  console.log(name + " has " + value[0] + " " + value[1]);
}

const trios: [string, [number, boolean]][] = [
  ["a", [1, true]],
  ["b", [2, false]],
  ["c", [3, false]]
];

trios.forEach(trio => logTrio(...trio)); // 正確

// 以下會報錯
trios.forEach(logTrio);

元組推導

TypeScript 通常將建立的陣列,視為可變長度陣列,而不是元組。如果解析到一個陣列,被使用作為變數的初始值或函式的回傳值,那麼將會假定成一個大小靈活的陣列,而非一個固定大小的元組。

例:

// 回傳型別:(string | number)[]
function firstCharAndSize(input: string) {
  return [input[0], input.length];
}

// firstChar 型別:string | number
// size 型別:string | number
const [firstChar, size] = firstCharAndSize("a");

TypeScript 中有兩種常見的方式,用來指定一個變數,應該是一個更具體的元組型別,即:「明確的元組型別」、「常數斷言元組」。

明確的元組型別

元組型別可以用在型別註記中,例如函式的回傳型別註記。如果函式被宣告為回傳元組型別,並且回傳陣列字面形式,則該陣列將被推斷為元組,例:

// 回傳型別:[string, number]
function firstTest(input: string): [string, number]{
  return [input[0], input.length];
}

// firstChar 型別:string
// size 型別:number
const [firstChar, size] = firstTest("abc");

常數斷言元組

在上述的明確型別中,註記輸入的元組型別會很麻煩。

TypeScript 提供一個 as const 運算符號作為替代方案,稱為常數斷言(const 斷言),原文為 const assertion,它可以放在資料之後。

const 斷言:告訴 TypeScript 在推斷其型別時,可能使用字面唯讀的形式分析資料。如果將運算符號放在一個陣列之後,則表示該陣列應被視為一個元組,例:

// 型別:(string | number)[]
const unionArray = [1157, "abc"];

// 型別:readonly [123, "def"]
const readonlyTuple = [123, "def"] as const;

留意 as const 斷言,不僅僅將大小靈活的陣列切換到固定大小的元組:它們還向 TypeScript 表明該元組是唯讀的,不能期望在其他地方做資料的修改。例:

// 允許修改,因為具有傳統的明確元組型別
const pairMutable: [number, string] = [1157, "ab"];
pairMutable[0] = 1247; // 正確

const pairConst = [1157, "cd"] as const;
// 以下錯誤:Cannot assign to '0' because it is a read-only property.
pairConst[0] = 11;

另外,函式回傳的唯讀的 [string, number],但外部使用的程式碼只關心此回傳元組中的資料:

// 回傳型別:readonly [string, number]
function firstCharAsConst(input: string){
 return [input[0], input.length] as const;
}

// firstChar 型別:string
// size 型別:number
const [firstChar, size] = firstCharAsConst("abc");

留言

發佈留言