處理 TypeScript
請支持這本書:購買捐贈
(廣告,請不要封鎖。)

19 輸入陣列



在本章中,我們將探討如何在 TypeScript 中輸入陣列。

19.1 陣列的角色

陣列可以在 JavaScript 中扮演下列角色(其中一種或多種組合)

TypeScript 提供各種輸入陣列的方式,以容納這兩個角色。我們將接著探討這些方式。

19.2 輸入陣列的方式

19.2.1 陣列角色「清單」:陣列類型文字與介面類型Array

陣列類型文字包含元素類型,後接[]。在以下程式碼中,陣列類型文字為string[]

// Each Array element has the type `string`:
const myStringArray: string[] = ['fee', 'fi', 'fo', 'fum'];

陣列類型文字是使用全域泛型介面類型Array的簡寫

const myStringArray: Array<string> = ['fee', 'fi', 'fo', 'fum'];

如果元素類型較為複雜,我們需要為陣列類型文字加上括號

(number|string)[]
(() => boolean)[]

泛型類型Array在這種情況下會比較好

Array<number|string>
Array<() => boolean>

19.2.2 陣列角色「元組」:元組類型文字

如果陣列有固定長度,且每個元素有不同的固定類型,取決於其位置,那麼我們可以使用元組類型字面值,例如 [string, string, boolean]

const yes: [string, string, boolean] = ['oui', 'sí', true];

19.2.3 同時也是類陣列的物件:具有索引簽章的介面

如果介面只有一個索引簽章,我們可以使用它來表示陣列

interface StringArray {
  [index: number]: string;
}
const strArr: StringArray = ['Huey', 'Dewey', 'Louie'];

同時具有索引簽章和屬性簽章的介面,僅適用於物件(因為索引元素和屬性需要同時定義)

interface FirstNamesAndLastName {
  [index: number]: string;
  lastName: string;
}

const ducks: FirstNamesAndLastName = {
  0: 'Huey',
  1: 'Dewey',
  2: 'Louie',
  lastName: 'Duck',
};

19.3 陷阱:類型推論並不總是能正確得到陣列類型

19.3.1 推論陣列類型很困難

由於陣列的兩個角色,TypeScript 無法總是猜測正確的類型。舉例來說,考慮以下陣列字面值,它被指定給變數 fields

const fields: Fields = [
  ['first', 'string', true],
  ['last', 'string', true],
  ['age', 'number', false],
];

fields 的最佳類型是什麼?以下都是合理的選擇

type Fields = Array<[string, string, boolean]>;
type Fields = Array<[string, ('string'|'number'), boolean]>;
type Fields = Array<Array<string|boolean>>;
type Fields = [
  [string, string, boolean],
  [string, string, boolean],
  [string, string, boolean],
];
type Fields = [
  [string, 'string', boolean],
  [string, 'string', boolean],
  [string, 'number', boolean],
];
type Fields = [
  Array<string|boolean>,
  Array<string|boolean>,
  Array<string|boolean>,
];

19.3.2 非空陣列字面值的類型推論

當我們使用非空陣列字面值時,TypeScript 的預設是推論清單類型(不是元組類型)

// %inferred-type: (string | number)[]
const arr = [123, 'abc'];

唉,這並不總是我們想要的

function func(p: [number, number]) {
  return p;
}
// %inferred-type: number[]
const pair1 = [1, 2];

// @ts-expect-error: Argument of type 'number[]' is not assignable to
// parameter of type '[number, number]'. [...]
func(pair1);

我們可以透過在 const 宣告中新增類型註解來修正這個問題,這會避免類型推論

const pair2: [number, number] = [1, 2];
func(pair2); // OK

19.3.3 空陣列字面值的類型推論

如果我們使用空陣列字面值初始化變數,那麼 TypeScript 最初會推論類型 any[],並在我們進行變更時逐步更新該類型

// %inferred-type: any[]
const arr1 = [];

arr1.push(123);
// %inferred-type: number[]
arr1;

arr1.push('abc');
// %inferred-type: (string | number)[]
arr1;

請注意,最初推論的類型不受稍後發生的事情影響。

如果我們使用指定而不是 .push(),事情會以相同的方式運作

// %inferred-type: any[]
const arr1 = [];

arr1[0] = 123;
// %inferred-type: number[]
arr1;

arr1[1] = 'abc';
// %inferred-type: (string | number)[]
arr1;

相反地,如果陣列字面值至少有一個元素,那麼元素類型是固定的,並且不會在稍後變更

// %inferred-type: number[]
const arr = [123];

// @ts-expect-error: Argument of type '"abc"' is not assignable to
// parameter of type 'number'. (2345)
arr.push('abc');

19.3.4 陣列和類型推論的 const 斷言

我們可以使用 const 斷言 為陣列字面值加上字尾

// %inferred-type: readonly ["igneous", "metamorphic", "sedimentary"]
const rockCategories =
  ['igneous', 'metamorphic', 'sedimentary'] as const;

我們宣告 rockCategories 不會變更。這有以下影響

以下是更多有和沒有 const 斷言的陣列字面值範例

// %inferred-type: readonly [1, 2, 3, 4]
const numbers1 = [1, 2, 3, 4] as const;
// %inferred-type: number[]
const numbers2 = [1, 2, 3, 4];

// %inferred-type: readonly [true, "abc"]
const booleanAndString1 = [true, 'abc'] as const;
// %inferred-type: (string | boolean)[]
const booleanAndString2 = [true, 'abc'];
19.3.4 const 斷言的潛在陷阱

const 斷言有兩個潛在陷阱。

首先,推論出的類型盡可能窄。這會對 let 宣告的變數造成問題:我們無法指定任何元組,除了我們用於初始化的元組

let arr = [1, 2] as const;

arr = [1, 2]; // OK

// @ts-expect-error: Type '3' is not assignable to type '2'. (2322)
arr = [1, 3];

其次,透過 as const 宣告的元組無法變異

let arr = [1, 2] as const;

// @ts-expect-error: Cannot assign to '1' because it is a read-only
// property. (2540)
arr[1] = 3;

這既不是優點也不是缺點,但我們需要知道它會發生。

19.4 陷阱:TypeScript 假設索引永遠不會超出範圍

每當我們透過索引存取陣列元素時,TypeScript 總是假設索引在範圍內(第 A 行)

const messages: string[] = ['Hello'];

// %inferred-type: string
const message = messages[3]; // (A)

由於這個假設,message 的類型是 string。而不是 undefinedundefined|string,正如我們可能預期的。

如果我們使用元組類型,我們會收到錯誤

const messages: [string] = ['Hello'];

// @ts-expect-error: Tuple type '[string]' of length '1' has no element
// at index '1'. (2493)
const message = messages[1];

as const 會有相同的效應,因為它會導致推論出元組類型。