TypeScript - 物件的型別:介面 Interface
什麼是介面(Interface)?
在物件導向程式語言中,介面(Interfaces)是一個很重要的概念。它用來定義物件的結構和行為。一個介面描述了一個物件應該有哪些屬性、方法以及其型別,類似於一個「契約」,告訴你的程式碼應該如何組織資料。
TypeScript 中的介面是一個非常有彈性的概念,不僅可以用來「對類別(Class)的一部分行為進行抽象」也常用來對「物件的形狀(Shape)進行描述」。
對類別的行為進行抽象:定義一個類別的行為和契約,即該類別需要實現特定的方法或行為。這樣的好處是可以確保不同的類別都具有相同的方法,讓程式碼更具可讀性和可維護性。
對物件的形狀進行描述:介面也可以用來描述物件的外觀和結構,即物件應擁有哪些屬性和方法。這種情況下,我們通常稱介面為「形狀(Shape)」。當物件符合介面的形狀時,它就被認為是該介面的實例。
範例
interface Person {
name: string;
age: number;
}
const alice: Person = {
name: 'Alice',
age: 25
}
在這個例子中,我們定義了名為 Person
的介面,Person
介面要求物件需要有:
name
屬性,字串型別age
屬性,數字型別
接著我們宣告了名為 alice
的物件,其型別為 Person
。這代表它需要符合 Person
介面的結構,也就是「tom
的形狀必須和 Person
一致」。
介面命名的慣例通常是使用 PascalCase(首字母大寫的駝峰式命名)。
有些開發者會在名稱前加上大寫的
I
作為前綴,用來表示它是一個介面(Interface),例如:IPerson
。這種命名方式在某些程式語言(如 C#)的開發者中比較常見。
賦值的時候,變數的形狀必須和介面的形狀保持一致。當定義的變數不遵守介面的「契約」時就會報錯:
interface Person {
name: string;
age: number;
}
//=== 例 1:少了 `age` 屬性
let person1: Person = {
name: 'Tom'
};
// 錯誤:Property 'age' is missing in type '{ name: string; }'.
//=== 例 2:多了 `gender` 屬性
let person2: Person = {
name: 'Alice',
age: 25,
gender: 'female'
}
// 錯誤:Object literal may only specify known properties, and 'gender' does not exist in type 'Person'.
//=== 例 3:`name` 屬性的型別錯誤
let person3: Person = {
name: 30,
age: 25
}
// 錯誤:Type 'number' is not assignable to type 'string'.
可是這樣也太不彈性了吧?TypeScript 在介面定義中除了上述的「確定屬性」之外,還可以使用其他特殊屬性:
可選屬性 Optional Properties:
propName?: type
任意屬性 Index Signature:
[propName: type]
唯讀屬性 Readonly Properties:
readonly propName: type
可選屬性 Optional Properties
當我們希望不需要完全匹配一個形狀的時候,可以使用可選屬性:?
。可選屬性允許我們在介面定義中指定某些屬性可以存在,也可以不存在。
使用 ?
符號來表示一個屬性是可選的(optional)。
當一個屬性被表示為可選屬性,代表即使該屬性不存在也不會造成型別錯誤。
interface Person {
name: string;
age?: number; // age 是可選屬性
email?: string; // email 是可選屬性
}
const person1: Person = {
name: 'Alice',
age: 25
}
const person2: Person = {
name: 'Tom',
email: 'tom@example.com'
}
可選屬性的常見應用
可選屬性在處理物件的時候很有彈性。特別是當你在處理不完整的資料,或是不同物件間的共享屬性時非常有用。
用戶資訊、填寫表單:當我們在獲取用戶資訊時,並不是所有資料都是必要的。例如在填表單時,有些欄位是必填的,有些是選填的,這種情況下就可以使用可選屬性來表示選填的欄位。
interface UserProfile { name: string; email: string; phone?: string; // 電話號碼是可選的 }
API 回應:從伺服器獲取資料時,不同 API 回應可能會包含不同的屬性,使用可選屬性來處理不同回應之間的差異。
interface ApiResponse { data: any; error?: string; // 錯誤訊息是可選的 }
函示參數:在呼叫函式時,有時候你只需要傳遞部分參數,其他參數則是可選的。這在處理多種使用情況的函式時很有用。
function sendMessage(message: string, recipient?: string){ // .. }
配置設定:當你需要配置設定時,某些選項可能是可選的,因為不是每個配置項都需要被指定。
interface AppConfig { theme: string; language: string; analyticsEnabled?: boolean; // 分析功能是可選的 }
任意屬性 Index Signature
在某些情況下,我們可能無法確定物件具有哪些屬性,或者物件可能有動態生成的屬性。這時候,我們會希望一個介面具有任意的屬性。
在 TypeScript 的介面中,除了可選屬性,還有一種特殊的屬性稱為「任意屬性」(Index Signature),它允許你定義物件可以具有的任意屬性。
使用 [propName: string]
來表示任意屬性 (Index Signatures):
interface Person {
name: string;
age?: number;
[propName: string]: any
}
let alice: Person = {
name: 'Alice',
gender: 'female'
}
在這個例子中,Person
介面包含了:
name
- 確定屬性age
- 可選屬性[propName: string]: any
- 任意屬性:表示我們可以為Person
類型的物件添加任意額外的屬性,且propName
必須是字串型別。在alice
這個物件中,即使我們添加了gender
作為額外的屬性,TypeScript 也不會報錯。
需注意,一旦定義了任意屬性,那麼確定屬性和可選屬性的型別都必須是它的型別的子集。換句話說,任意屬性的型別必須要涵蓋其他屬性的型別才行。
interface Person {
name: string;
age?: number;
[key: string]: string;
}
let alice: Person = {
name: 'Alice',
age: 25,
gender: 'female'
}
// index.ts:3:5 - error TS2411: Property 'age' of type 'number | undefined' is not assignable to 'string' index type 'string'.
// index.ts:7:5 - error TS2322: Type '{ name: string; age: number; gender: string; }' is not assignable to type 'Person'.
// Property 'age' is incompatible with index signature.
// Type 'number' is not assignable to type 'string'.
在這個例子中,任意屬性 [key: string]
的屬性值型別被改成 string
,但是 age
的值卻是 number
,並不屬於 string
的子屬性,因此 TypeScript 會報錯。
當我們使用 [key: string]: string
來定義介面規範時,就表示物件裡面任何一個屬性值的型別都必須是 string
或是 string
的子型別。
一個介面中只能定義一個任意屬性。
如果想讓任意屬性的屬性值有多個型別,可以將任意屬性指定為聯合型別:
interface Person {
name: string;
age?: number;
[key: string]: string | number; // 聯合型別,可以是 string 或 number 型別
}
任意屬性的常見應用
任意屬性在處理動態生成的資料結構時相當有用。
動態屬性:當物件的屬性名稱不確定時,你可以使用任意屬性來處理這種情況。這在處理不同形式的資料時很有彈性。
```typescript interface DynamicData {
}
const measurements: DynamicData = { height: 180, weight: 75 };
2. **JSON 解析**:解析 JSON 時,其中的屬性可能會隨著不同的資料而變化。使用任意屬性可以處理這種情況,確保你可以讀取並處理不同形式的 JSON 數據。
```typescript
interface JsonData {
[key: string]: any;
}
const jsonData: JsonData = {
name: "Alice",
age: 25,
isActive: true
};
唯讀屬性 Readonly Properties
有時候我們會希望物件中的一些欄位只能在建立的時候被賦值,一但初始化後就不能再被修改,這時候可以使用 readonly
定義唯讀屬性。
在 TypeScript 中,唯讀屬性是指在物件被創建後,該屬性的值不能再被修改。這可以用來確保物件的某些屬性在被設定後不會被意外地更改,增加程式碼的穩定性和可靠性。
可以在介面或類別的屬性名稱前面加上 readonly
:
interface Person {
readonly id: number; // 唯讀屬性
name: string;
age?: number;
[propName: string]: any;
}
const alice: Person = {
id: 123,
name: 'Alice',
age: 25
}
在這個例子中,Person
介面包含了:
id
- 唯讀屬性name
- 確定age
- 可選屬性
我們在 id
屬性前面加上了 readonly
關鍵字,表示該屬性被定義為唯讀的。這表示當我們創建 alice
物件後,id
屬性的值就不能再被修改。
當我們嘗試修改 alice
的 id
屬性的值時,編譯器就會報錯,因為該屬性被定義為唯讀,不允許修改:
alice.id = 345;
// error TS2540: Cannot assign to 'id' because it is a read-only property.
需注意的是,唯讀的約束是存在於第一次給「物件」賦值的時候,而不是第一次給「唯讀屬性」賦值的時候:
interface Person {
readonly id: number;
name: string;
age?: number;
[propName: string]: any;
}
const alice: Person = {
name: 'Alice',
gender: 'female'
};
alice.id = 1;
// index.ts:8:7 - error TS2741: Property 'id' is missing in type '{ name: string; gender: string; }' but required in type 'Person'.
// index.ts:13:7 - error TS2540: Cannot assign to 'id' because it is a read-only property.
上述例子中,錯誤訊息有兩處。
第一個是在對 alice
賦值時,沒有給 id
賦值:
// index.ts:8:7 - error TS2741: Property 'id' is missing in type '{ name: string; gender: string; }' but required in type 'Person'.
第二個是在給 alice.id
賦值的時候,由於它是唯讀屬性,所以報錯:
// index.ts:13:7 - error TS2540: Cannot assign to 'id' because it is a read-only property.
唯讀屬性的常見應用
保護物件免於被修改:有些屬性在物件創建後應該保持不變。使用唯讀屬性可以確保這些值不會被改變,從而防止錯誤。例如:產品的 id 在創建後就不應被修改:
interface Product { readonly id: number; name: string; price: number; } function updateProduct(product: Product, newName: string) { // 以下操作會引發編譯錯誤,因為 id 是唯讀屬性 product.id = 123; product.name = newName; }
可信賴的資料:當你有一些在運行時設定,但在後續不應該被改變的資料時,可以使用唯讀屬性確保這些資料不被修改。例如當你在程式中設定一些常數或設定值時:
interface Config { readonly apiUrl: string; readonly maxRequestsPerSecond: number; } const config: Config = { apiUrl: "https://api.example.com", maxRequestsPerSecond: 10 };
共享物件:在多個地方使用同一個物件時,使用唯讀屬性可以確保這個物件的值不會在其他地方被更改。例如在多個模組或組件中共享配置設定時,使用唯讀屬性可以確保配置不會在其他地方被更改:
export interface AppConfiguration { readonly theme: string; readonly language: string; } const defaultConfig: AppConfiguration = { theme: "light", language: "en" }; // 在其他模組中引入並使用 defaultConfig