物件型別

在 JavaScript 中,我們分組和傳遞資料的基本方式是透過物件。在 TypeScript 中,我們透過物件型別 (object types) 來表示它們。

如我們所見,它們可以是匿名的

ts
function greet(person: { name: string; age: number }) {
return "Hello " + person.name;
}
Try

也可以透過使用 interface (介面)

ts
interface Person {
name: string;
age: number;
}
 
function greet(person: Person) {
return "Hello " + person.name;
}
Try

或類型別名 (type alias) 來命名

ts
type Person = {
name: string;
age: number;
};
 
function greet(person: Person) {
return "Hello " + person.name;
}
Try

在上面這三個示例中,我們編寫的函式所接收的物件都包含 name(必須是 string 型別)和 age(必須是 number 型別)屬性。

快速參考

我們提供了 typeinterface 的速查表,如果您想快速瀏覽日常常用的語法,可以參考這些表格。

屬性修飾符

物件型別中的每個屬性都可以指定幾件事:型別、屬性是否可選,以及屬性是否可寫。

可選屬性

很多時候,我們會處理一些屬性可能未設定的物件。在這種情況下,我們可以透過在屬性名後面新增問號 (?) 將這些屬性標記為可選的

ts
interface PaintOptions {
shape: Shape;
xPos?: number;
yPos?: number;
}
 
function paintShape(opts: PaintOptions) {
// ...
}
 
const shape = getShape();
paintShape({ shape });
paintShape({ shape, xPos: 100 });
paintShape({ shape, yPos: 100 });
paintShape({ shape, xPos: 100, yPos: 100 });
Try

在這個示例中,xPosyPos 都被視為可選。我們可以選擇提供它們中的任何一個,因此上面對 paintShape 的每次呼叫都是有效的。可選性真正說明的是:如果該屬性確實被設定了,它最好具有指定的型別。

我們也可以讀取這些屬性——但在 strictNullChecks 開啟的情況下,TypeScript 會告訴我們它們可能為 undefined

ts
function paintShape(opts: PaintOptions) {
let xPos = opts.xPos;
(property) PaintOptions.xPos?: number | undefined
let yPos = opts.yPos;
(property) PaintOptions.yPos?: number | undefined
// ...
}
Try

在 JavaScript 中,即使屬性從未被設定,我們仍然可以訪問它——它只會返回 undefined 值。我們可以透過檢查 undefined 來專門處理它。

ts
function paintShape(opts: PaintOptions) {
let xPos = opts.xPos === undefined ? 0 : opts.xPos;
let xPos: number
let yPos = opts.yPos === undefined ? 0 : opts.yPos;
let yPos: number
// ...
}
Try

請注意,這種為未指定值設定預設值的模式非常常見,以至於 JavaScript 提供了支援它的語法。

ts
function paintShape({ shape, xPos = 0, yPos = 0 }: PaintOptions) {
console.log("x coordinate at", xPos);
(parameter) xPos: number
console.log("y coordinate at", yPos);
(parameter) yPos: number
// ...
}
Try

在這裡,我們為 paintShape 的引數使用了解構模式,併為 xPosyPos 提供了預設值。現在,xPosyPospaintShape 的函式體內確定存在,但對於任何 paintShape 的呼叫者來說它們依然是可選的。

請注意,目前無法在解構模式中放置型別註解。這是因為以下語法在 JavaScript 中已經具有不同的含義。

ts
function draw({ shape: Shape, xPos: number = 100 /*...*/ }) {
render(shape);
Cannot find name 'shape'. Did you mean 'Shape'?2552Cannot find name 'shape'. Did you mean 'Shape'?
render(xPos);
Cannot find name 'xPos'.2304Cannot find name 'xPos'.
}
Try

在物件解構模式中,shape: Shape 的意思是“獲取屬性 shape 並將其在本地重新定義為名為 Shape 的變數”。同樣,xPos: number 會建立一個名為 number 的變數,其值基於引數的 xPos

readonly 屬性

屬性在 TypeScript 中也可以被標記為 readonly。雖然這不會在執行時改變任何行為,但被標記為 readonly 的屬性在型別檢查期間不能被寫入。

ts
interface SomeType {
readonly prop: string;
}
 
function doSomething(obj: SomeType) {
// We can read from 'obj.prop'.
console.log(`prop has the value '${obj.prop}'.`);
 
// But we can't re-assign it.
obj.prop = "hello";
Cannot assign to 'prop' because it is a read-only property.2540Cannot assign to 'prop' because it is a read-only property.
}
Try

使用 readonly 修飾符並不一定意味著一個值是完全不可變的——換句話說,其內部內容不一定不能改變。它只是意味著屬性本身不能被重新賦值。

ts
interface Home {
readonly resident: { name: string; age: number };
}
 
function visitForBirthday(home: Home) {
// We can read and update properties from 'home.resident'.
console.log(`Happy birthday ${home.resident.name}!`);
home.resident.age++;
}
 
function evict(home: Home) {
// But we can't write to the 'resident' property itself on a 'Home'.
home.resident = {
Cannot assign to 'resident' because it is a read-only property.2540Cannot assign to 'resident' because it is a read-only property.
name: "Victor the Evictor",
age: 42,
};
}
Try

管理對 readonly 所隱含意義的預期非常重要。在開發過程中,它對於向 TypeScript 表示物件應如何使用非常有用。當檢查兩個型別是否相容時,TypeScript 不會考慮這些型別的屬性是否為 readonly,因此 readonly 屬性也可以透過別名發生改變。

ts
interface Person {
name: string;
age: number;
}
 
interface ReadonlyPerson {
readonly name: string;
readonly age: number;
}
 
let writablePerson: Person = {
name: "Person McPersonface",
age: 42,
};
 
// works
let readonlyPerson: ReadonlyPerson = writablePerson;
 
console.log(readonlyPerson.age); // prints '42'
writablePerson.age++;
console.log(readonlyPerson.age); // prints '43'
Try

使用對映修飾符 (mapping modifiers),你可以移除 readonly 屬性。

索引簽名 (Index Signatures)

有時你無法提前知道型別所有屬性的名稱,但你卻知道值的形狀。

在這種情況下,你可以使用索引簽名來描述可能的值型別,例如

ts
interface StringArray {
[index: number]: string;
}
 
const myArray: StringArray = getStringArray();
const secondItem = myArray[1];
const secondItem: string
Try

在上面,我們有一個包含索引簽名的 StringArray 介面。這個索引簽名規定:當 StringArraynumber 索引時,它將返回一個 string

索引簽名屬性僅允許使用以下型別:stringnumbersymbol、模板字串模式,以及由這些型別組成的聯合型別。

支援多種型別的索引器是可能的...

支援多種型別的索引器是可能的。請注意,當同時使用 numberstring 索引器時,從數字索引器返回的型別必須是字串索引器返回型別的子型別。這是因為當使用 number 進行索引時,JavaScript 實際上會在索引物件之前將其轉換為 string。這意味著使用 100(一個 number)索引與使用 "100"(一個 string)索引是一樣的,因此兩者必須保持一致。

ts
interface Animal {
name: string;
}
 
interface Dog extends Animal {
breed: string;
}
 
// Error: indexing with a numeric string might get you a completely separate type of Animal!
interface NotOkay {
[x: number]: Animal;
'number' index type 'Animal' is not assignable to 'string' index type 'Dog'.2413'number' index type 'Animal' is not assignable to 'string' index type 'Dog'.
[x: string]: Dog;
}
Try

雖然字串索引簽名是描述“字典”模式的強大方式,但它們也強制要求所有屬性都匹配其返回型別。這是因為字串索引聲明瞭 obj.property 也可以作為 obj["property"] 訪問。在下面的示例中,name 的型別與字串索引的型別不匹配,型別檢查器會報錯

ts
interface NumberDictionary {
[index: string]: number;
 
length: number; // ok
name: string;
Property 'name' of type 'string' is not assignable to 'string' index type 'number'.2411Property 'name' of type 'string' is not assignable to 'string' index type 'number'.
}
Try

然而,如果索引簽名是屬性型別的聯合,則不同型別的屬性是可以接受的

ts
interface NumberOrStringDictionary {
[index: string]: number | string;
length: number; // ok, length is a number
name: string; // ok, name is a string
}
Try

最後,你可以使索引簽名 readonly 以防止對其索引進行賦值

ts
interface ReadonlyStringArray {
readonly [index: number]: string;
}
 
let myArray: ReadonlyStringArray = getReadOnlyStringArray();
myArray[2] = "Mallory";
Index signature in type 'ReadonlyStringArray' only permits reading.2542Index signature in type 'ReadonlyStringArray' only permits reading.
Try

你不能設定 myArray[2],因為索引簽名是 readonly 的。

多餘屬性檢查 (Excess Property Checks)

物件被賦值給型別的位置和方式會在型別系統中產生差異。其中一個關鍵例子是多餘屬性檢查,它會在物件建立並賦值給物件型別時,更徹底地校驗該物件。

ts
interface SquareConfig {
color?: string;
width?: number;
}
 
function createSquare(config: SquareConfig): { color: string; area: number } {
return {
color: config.color || "red",
area: config.width ? config.width * config.width : 20,
};
}
 
let mySquare = createSquare({ colour: "red", width: 100 });
Object literal may only specify known properties, but 'colour' does not exist in type 'SquareConfig'. Did you mean to write 'color'?2561Object literal may only specify known properties, but 'colour' does not exist in type 'SquareConfig'. Did you mean to write 'color'?
Try

注意傳給 createSquare 的引數拼寫成了 colour 而不是 color。在純 JavaScript 中,這種情況會靜默失敗。

你可能會認為這個程式是正確輸入的,因為 width 屬性是相容的,沒有 color 屬性存在,且額外的 colour 屬性並不重要。

然而,TypeScript 的立場是這段程式碼中可能存在 bug。物件字面量在被賦值給其他變數或作為引數傳遞時會得到特殊處理,並進行多餘屬性檢查。如果一個物件字面量擁有“目標型別”所沒有的任何屬性,你將會得到一個錯誤

ts
let mySquare = createSquare({ colour: "red", width: 100 });
Object literal may only specify known properties, but 'colour' does not exist in type 'SquareConfig'. Did you mean to write 'color'?2561Object literal may only specify known properties, but 'colour' does not exist in type 'SquareConfig'. Did you mean to write 'color'?
Try

繞過這些檢查實際上非常簡單。最簡單的方法是使用型別斷言 (type assertion)

ts
let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);
Try

然而,如果你確定物件可以包含一些以特殊方式使用的額外屬性,更好的方法是新增一個字串索引簽名。如果 SquareConfig 可以擁有上述型別的 colorwidth 屬性,但也可以擁有任意數量的其他屬性,那麼我們可以這樣定義它

ts
interface SquareConfig {
color?: string;
width?: number;
[propName: string]: unknown;
}
Try

這裡我們說明 SquareConfig 可以擁有任意數量的屬性,只要它們不是 colorwidth,它們的型別就不重要。

繞過這些檢查的最後一種方法(可能會讓人有些驚訝)是將物件賦值給另一個變數:由於賦值給 squareOptions 不會觸發多餘屬性檢查,編譯器不會報錯

ts
let squareOptions = { colour: "red", width: 100 };
let mySquare = createSquare(squareOptions);
Try

只要 squareOptionsSquareConfig 之間有共同的屬性,上述解決方法就會有效。在這個例子中,共同屬性是 width。但是,如果變數沒有任何共同的物件屬性,它將會失敗。例如

ts
let squareOptions = { colour: "red" };
let mySquare = createSquare(squareOptions);
Type '{ colour: string; }' has no properties in common with type 'SquareConfig'.2559Type '{ colour: string; }' has no properties in common with type 'SquareConfig'.
Try

請記住,對於像上面那樣的簡單程式碼,你可能不應該試圖“繞過”這些檢查。對於具有方法和持有狀態的更復雜的物件字面量,你可能需要記住這些技巧,但大多數多餘屬性錯誤實際上都是 bug。

這意味著如果你在處理類似選項物件(option bags)時遇到了多餘屬性檢查問題,你可能需要修改一些型別定義。在這種情況下,如果向 createSquare 傳遞一個同時具有 colorcolour 屬性的物件是可以的,那麼你應該修復 SquareConfig 的定義來反映這一點。

擴充套件型別 (Extending Types)

擁有作為其他型別更具體版本的型別是非常常見的。例如,我們可能有一個 BasicAddress 型別,它描述了在美國發送信件和包裹所需的欄位。

ts
interface BasicAddress {
name?: string;
street: string;
city: string;
country: string;
postalCode: string;
}
Try

在某些情況下這已經足夠了,但如果地址所在的建築物有多個單元,地址通常會關聯一個單元編號。那麼我們可以描述一個 AddressWithUnit

ts
interface AddressWithUnit {
name?: string;
unit: string;
street: string;
city: string;
country: string;
postalCode: string;
}
Try

這樣做完成了任務,但缺點是我們不得不重複 BasicAddress 中的所有其他欄位,而我們的更改純粹是累加性的。相反,我們可以擴充套件原始的 BasicAddress 型別,並只新增 AddressWithUnit 所特有的新欄位。

ts
interface BasicAddress {
name?: string;
street: string;
city: string;
country: string;
postalCode: string;
}
 
interface AddressWithUnit extends BasicAddress {
unit: string;
}
Try

interface 上的 extends 關鍵字允許我們有效地從其他命名型別複製成員,並新增我們想要的任何新成員。這對於減少我們需要編寫的型別宣告樣板程式碼,以及表明某些相同屬性的不同宣告之間可能存在關聯的意圖非常有用。例如,AddressWithUnit 不需要重複 street 屬性,而且由於 street 源自 BasicAddress,讀者會知道這兩個型別在某種程度上是相關的。

interface 也可以擴充套件自多個型別。

ts
interface Colorful {
color: string;
}
 
interface Circle {
radius: number;
}
 
interface ColorfulCircle extends Colorful, Circle {}
 
const cc: ColorfulCircle = {
color: "red",
radius: 42,
};
Try

交叉型別 (Intersection Types)

interface 允許我們透過擴充套件來從其他型別構建新型別。TypeScript 提供了另一種稱為交叉型別的構造,主要用於組合現有的物件型別。

交叉型別是使用 & 運算子定義的。

ts
interface Colorful {
color: string;
}
interface Circle {
radius: number;
}
 
type ColorfulCircle = Colorful & Circle;
Try

在這裡,我們將 ColorfulCircle 進行了交叉,產生了一個同時擁有 Colorful Circle 所有成員的新型別。

ts
function draw(circle: Colorful & Circle) {
console.log(`Color was ${circle.color}`);
console.log(`Radius was ${circle.radius}`);
}
 
// okay
draw({ color: "blue", radius: 42 });
 
// oops
draw({ color: "red", raidus: 42 });
Object literal may only specify known properties, but 'raidus' does not exist in type 'Colorful & Circle'. Did you mean to write 'radius'?2561Object literal may only specify known properties, but 'raidus' does not exist in type 'Colorful & Circle'. Did you mean to write 'radius'?
Try

介面擴充套件 vs. 交叉型別

我們剛剛看了兩種組合型別的方法,它們很相似,但實際上有細微的差別。對於介面,我們可以使用 extends 子句從其他型別擴充套件,我們也可以透過交叉來實現類似的事情,並用類型別名命名結果。兩者之間的主要區別在於如何處理衝突,這種區別通常是你在介面和交叉類型別名之間做出選擇的主要原因之一。

如果同名介面被定義,TypeScript 將嘗試在屬性相容的情況下合併它們。如果屬性不相容(即它們具有相同的屬性名但型別不同),TypeScript 將丟擲錯誤。

在交叉型別的情況下,具有不同型別的屬性將自動合併。當稍後使用該型別時,TypeScript 會期望該屬性同時滿足兩種型別,這可能會產生意想不到的結果。

例如,以下程式碼會丟擲錯誤,因為屬性是不相容的

ts
interface Person {
name: string;
}
interface Person {
name: number;
}

相比之下,以下程式碼可以編譯,但會導致 never 型別

ts
interface Person1 {
name: string;
}
 
interface Person2 {
name: number;
}
 
type Staff = Person1 & Person2
 
declare const staffer: Staff;
staffer.name;
(property) name: never
Try

在這種情況下,Staff 需要 name 屬性既是字串又是數字,這導致屬性的型別為 never

泛型物件型別 (Generic Object Types)

讓我們想象一個 Box 型別,它可以包含任何值——stringnumberGiraffe 等等。

ts
interface Box {
contents: any;
}
Try

目前,contents 屬性被型別化為 any,這雖然有效,但可能導致以後的事故。

我們也可以使用 unknown,但這意味著在已經知道 contents 型別的情況下,我們需要進行預防性檢查,或者使用容易出錯的型別斷言。

ts
interface Box {
contents: unknown;
}
 
let x: Box = {
contents: "hello world",
};
 
// we could check 'x.contents'
if (typeof x.contents === "string") {
console.log(x.contents.toLowerCase());
}
 
// or we could use a type assertion
console.log((x.contents as string).toLowerCase());
Try

一種型別安全的方法是為每種 contents 型別構建不同的 Box 型別。

ts
interface NumberBox {
contents: number;
}
 
interface StringBox {
contents: string;
}
 
interface BooleanBox {
contents: boolean;
}
Try

但這意味著我們必須建立不同的函式或函式過載來操作這些型別。

ts
function setContents(box: StringBox, newContents: string): void;
function setContents(box: NumberBox, newContents: number): void;
function setContents(box: BooleanBox, newContents: boolean): void;
function setContents(box: { contents: any }, newContents: any) {
box.contents = newContents;
}
Try

那會有大量的樣板程式碼。此外,我們以後可能需要引入新的型別和過載。這很令人沮喪,因為我們的盒子型別和過載實際上都是一樣的。

相反,我們可以建立一個泛型 Box 型別,它聲明瞭一個型別引數

ts
interface Box<Type> {
contents: Type;
}
Try

你可以將其理解為“Box of Type 是其 contents 具有 Type 型別的東西”。稍後,當我們引用 Box 時,必須在 Type 的位置提供一個型別實參

ts
let box: Box<string>;
Try

Box 看作是一個真實型別的模板,其中 Type 是一個佔位符,它將被其他型別替換。當 TypeScript 看到 Box<string> 時,它會將 Box<Type> 中的每個 Type 例項替換為 string,並最終處理類似於 { contents: string } 的東西。換句話說,Box<string> 和我們之前的 StringBox 的作用是一樣的。

ts
interface Box<Type> {
contents: Type;
}
interface StringBox {
contents: string;
}
 
let boxA: Box<string> = { contents: "hello" };
boxA.contents;
(property) Box<string>.contents: string
 
let boxB: StringBox = { contents: "world" };
boxB.contents;
(property) StringBox.contents: string
Try

Box 是可重用的,因為 Type 可以被任何東西替代。這意味著當我們為新型別需要一個盒子時,根本不需要宣告一個新的 Box 型別(儘管如果願意,我們當然可以這樣做)。

ts
interface Box<Type> {
contents: Type;
}
 
interface Apple {
// ....
}
 
// Same as '{ contents: Apple }'.
type AppleBox = Box<Apple>;
Try

這也意味著我們可以透過使用泛型函式來完全避免過載。

ts
function setContents<Type>(box: Box<Type>, newContents: Type) {
box.contents = newContents;
}
Try

值得注意的是,類型別名也可以是泛型的。我們可以定義我們新的 Box<Type> 介面,即

ts
interface Box<Type> {
contents: Type;
}
Try

透過使用類型別名來代替

ts
type Box<Type> = {
contents: Type;
};
Try

由於類型別名不像介面那樣只能描述物件型別,我們也可以用它們來編寫其他型別的泛型輔助型別。

ts
type OrNull<Type> = Type | null;
 
type OneOrMany<Type> = Type | Type[];
 
type OneOrManyOrNull<Type> = OrNull<OneOrMany<Type>>;
type OneOrManyOrNull<Type> = OneOrMany<Type> | null
 
type OneOrManyOrNullStrings = OneOrManyOrNull<string>;
type OneOrManyOrNullStrings = OneOrMany<string> | null
Try

我們稍後會回到類型別名。

Array 型別

泛型物件型別通常是某種容器型別,它們獨立於其所包含元素的型別工作。資料結構以這種方式工作是理想的,以便它們可以在不同的資料型別中重用。

事實證明,我們在整個手冊中一直使用的就是這樣一個型別:Array 型別。每當我們寫出像 number[]string[] 這樣的型別時,這實際上只是 Array<number>Array<string> 的簡寫。

ts
function doSomething(value: Array<string>) {
// ...
}
 
let myArray: string[] = ["hello", "world"];
 
// either of these work!
doSomething(myArray);
doSomething(new Array("hello", "world"));
Try

就像上面的 Box 型別一樣,Array 本身也是一個泛型型別。

ts
interface Array<Type> {
/**
* Gets or sets the length of the array.
*/
length: number;
 
/**
* Removes the last element from an array and returns it.
*/
pop(): Type | undefined;
 
/**
* Appends new elements to an array, and returns the new length of the array.
*/
push(...items: Type[]): number;
 
// ...
}
Try

現代 JavaScript 還提供了其他通用的資料結構,如 Map<K, V>Set<T>Promise<T>。這一切真正意味著,由於 MapSetPromise 的行為方式,它們可以與任何型別的集合一起工作。

ReadonlyArray 型別

ReadonlyArray 是一種特殊的型別,用於描述不應更改的陣列。

ts
function doStuff(values: ReadonlyArray<string>) {
// We can read from 'values'...
const copy = values.slice();
console.log(`The first value is ${values[0]}`);
 
// ...but we can't mutate 'values'.
values.push("hello!");
Property 'push' does not exist on type 'readonly string[]'.2339Property 'push' does not exist on type 'readonly string[]'.
}
Try

就像屬性的 readonly 修飾符一樣,它主要是一個我們可以用來表達意圖的工具。當我們看到一個返回 ReadonlyArray 的函式時,它告訴我們內容是不允許更改的;而當我們看到一個消費 ReadonlyArray 的函式時,它告訴我們,我們可以將任何陣列傳遞給該函式,而不必擔心它會改變其內容。

Array 不同,沒有我們可以使用的 ReadonlyArray 建構函式。

ts
new ReadonlyArray("red", "green", "blue");
'ReadonlyArray' only refers to a type, but is being used as a value here.2693'ReadonlyArray' only refers to a type, but is being used as a value here.
Try

相反,我們可以將常規的 Array 分配給 ReadonlyArray

ts
const roArray: ReadonlyArray<string> = ["red", "green", "blue"];
Try

正如 TypeScript 為 Array<Type> 提供了 Type[] 的簡寫語法一樣,它也為 ReadonlyArray<Type> 提供了 readonly Type[] 的簡寫語法。

ts
function doStuff(values: readonly string[]) {
// We can read from 'values'...
const copy = values.slice();
console.log(`The first value is ${values[0]}`);
 
// ...but we can't mutate 'values'.
values.push("hello!");
Property 'push' does not exist on type 'readonly string[]'.2339Property 'push' does not exist on type 'readonly string[]'.
}
Try

最後要注意的一點是,與 readonly 屬性修飾符不同,常規 ArrayReadonlyArray 之間的可分配性不是雙向的。

ts
let x: readonly string[] = [];
let y: string[] = [];
 
x = y;
y = x;
The type 'readonly string[]' is 'readonly' and cannot be assigned to the mutable type 'string[]'.4104The type 'readonly string[]' is 'readonly' and cannot be assigned to the mutable type 'string[]'.
Try

元組型別 (Tuple Types)

元組型別 是另一種 Array 型別,它準確知道它包含多少個元素,以及在特定位置包含哪些型別。

ts
type StringNumberPair = [string, number];
Try

這裡,StringNumberPair 是一個由 stringnumber 組成的元組型別。像 ReadonlyArray 一樣,它在執行時沒有表示,但對 TypeScript 意義重大。對於型別系統,StringNumberPair 描述了那些索引 0 包含 string 且索引 1 包含 number 的陣列。

ts
function doSomething(pair: [string, number]) {
const a = pair[0];
const a: string
const b = pair[1];
const b: number
// ...
}
 
doSomething(["hello", 42]);
Try

如果我們嘗試在超過元素數量的地方索引,我們將會得到一個錯誤。

ts
function doSomething(pair: [string, number]) {
// ...
 
const c = pair[2];
Tuple type '[string, number]' of length '2' has no element at index '2'.2493Tuple type '[string, number]' of length '2' has no element at index '2'.
}
Try

我們也可以使用 JavaScript 的陣列解構來解構元組

ts
function doSomething(stringHash: [string, number]) {
const [inputString, hash] = stringHash;
 
console.log(inputString);
const inputString: string
 
console.log(hash);
const hash: number
}
Try

元組型別在高度基於約定的 API 中非常有用,其中每個元素的含義都很“明顯”。這賦予了我們在解構變數時命名任何我們想要的變數的靈活性。在上面的示例中,我們能夠將元素 01 命名為任何我們想要的名字。

然而,由於並非每個使用者對“明顯”的看法都相同,可能值得重新考慮使用帶有描述性屬性名稱的物件是否對你的 API 更好。

除了這些長度檢查之外,像這樣的簡單元組型別等同於 Array 的版本,這些版本為特定索引宣告屬性,並使用數字字面量型別宣告 length

ts
interface StringNumberPair {
// specialized properties
length: 2;
0: string;
1: number;
 
// Other 'Array<string | number>' members...
slice(start?: number, end?: number): Array<string | number>;
}
Try

另一個你可能感興趣的事情是,元組可以透過寫一個問號(? 在元素型別之後)擁有可選屬性。可選元組元素只能放在最後,並且也會影響 length 的型別。

ts
type Either2dOr3d = [number, number, number?];
 
function setCoordinate(coord: Either2dOr3d) {
const [x, y, z] = coord;
const z: number | undefined
 
console.log(`Provided coordinates had ${coord.length} dimensions`);
(property) length: 2 | 3
}
Try

元組也可以有剩餘元素 (rest elements),它們必須是陣列/元組型別。

ts
type StringNumberBooleans = [string, number, ...boolean[]];
type StringBooleansNumber = [string, ...boolean[], number];
type BooleansStringNumber = [...boolean[], string, number];
Try
  • StringNumberBooleans 描述了一個元組,其前兩個元素分別是 stringnumber,但後面可能還有任意數量的 boolean
  • StringBooleansNumber 描述了一個元組,其第一個元素是 string,然後是任意數量的 boolean,並以 number 結尾。
  • BooleansStringNumber 描述了一個元組,其開始元素是任意數量的 boolean,並以 stringnumber 結尾。

帶有剩餘元素的元組沒有固定的“長度”——它只有一組在不同位置上眾所周知的元素。

ts
const a: StringNumberBooleans = ["hello", 1];
const b: StringNumberBooleans = ["beautiful", 2, true];
const c: StringNumberBooleans = ["world", 3, true, false, true, false, true];
Try

為什麼可選元素和剩餘元素有用?好吧,它允許 TypeScript 將元組與引數列表對應起來。元組型別可以用在剩餘引數和實參中,因此以下內容

ts
function readButtonInput(...args: [string, number, ...boolean[]]) {
const [name, version, ...input] = args;
// ...
}
Try

基本上等同於

ts
function readButtonInput(name: string, version: number, ...input: boolean[]) {
// ...
}
Try

當你想要使用剩餘引數獲取可變數量的引數,並且需要最少數量的元素,但又不想引入中間變數時,這很方便。

readonly 元組型別

關於元組型別的最後一點說明——元組型別有 readonly 變體,可以透過在它們前面加上 readonly 修飾符來指定——就像陣列簡寫語法一樣。

ts
function doSomething(pair: readonly [string, number]) {
// ...
}
Try

正如你所預料的那樣,在 TypeScript 中,不允許向 readonly 元組的任何屬性進行寫入。

ts
function doSomething(pair: readonly [string, number]) {
pair[0] = "hello!";
Cannot assign to '0' because it is a read-only property.2540Cannot assign to '0' because it is a read-only property.
}
Try

大多數程式碼中的元組通常在建立後就不會被修改,因此儘可能將型別標註為 readonly 元組是一個很好的預設做法。考慮到帶有 const 斷言的陣列字面量將被推斷為 readonly 元組型別,這一點也很重要。

ts
let point = [3, 4] as const;
 
function distanceFromOrigin([x, y]: [number, number]) {
return Math.sqrt(x ** 2 + y ** 2);
}
 
distanceFromOrigin(point);
Argument of type 'readonly [3, 4]' is not assignable to parameter of type '[number, number]'. The type 'readonly [3, 4]' is 'readonly' and cannot be assigned to the mutable type '[number, number]'.2345Argument of type 'readonly [3, 4]' is not assignable to parameter of type '[number, number]'. The type 'readonly [3, 4]' is 'readonly' and cannot be assigned to the mutable type '[number, number]'.
Try

這裡,distanceFromOrigin 從不修改其元素,但期望一個可變元組。由於 point 的型別被推斷為 readonly [3, 4],它將不相容 [number, number],因為該型別無法保證 point 的元素不會被變異。

TypeScript 文件是一個開源專案。請透過傳送 Pull Request 幫助我們改進這些頁面 ❤

此頁面的貢獻者
DRDaniel Rosenwasser (52)
OTOrta Therox (16)
338elements  (2)
BRBruce Robertson (2)
ARAlan Rempel (2)
23+

最後更新:2026 年 3 月 27 日