# TypeScript - 物件的型別:介面 Interface
## 什麼是介面(Interface)?
在物件導向程式語言中,介面(Interfaces)是一個很重要的概念。它用來**定義物件的結構和行為**。一個介面描述了一個物件應該有哪些屬性、方法以及其型別,類似於一個「契約」,告訴你的程式碼應該如何組織資料。
TypeScript 中的介面是一個非常有彈性的概念,不僅可以用來「對類別(Class)的一部分行為進行抽象」也常用來對「物件的形狀(Shape)進行描述」。
1. **對類別的行為進行抽象**:定義一個類別的行為和契約,即該類別需要實現特定的方法或行為。這樣的好處是可以確保不同的類別都具有相同的方法,讓程式碼更具可讀性和可維護性。
2. **對物件的形狀進行描述**:介面也可以用來描述物件的外觀和結構,即物件應擁有哪些屬性和方法。這種情況下,我們通常稱介面為「形狀(Shape)」。當物件符合介面的形狀時,它就被認為是該介面的實例。
### 範例
```typescript
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#)的開發者中比較常見。
**賦值的時候,變數的形狀必須和介面的形狀保持一致**。當定義的變數不遵守介面的「契約」時就會報錯:
```typescript
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)**。
當一個屬性被表示為可選屬性,代表即使該屬性不存在也不會造成型別錯誤。
```typescript
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'
}
```
### 可選屬性的常見應用
可選屬性在處理物件的時候很有彈性。特別是當你在處理不完整的資料,或是不同物件間的共享屬性時非常有用。
1. 用戶資訊、填寫表單:當我們在獲取用戶資訊時,並不是所有資料都是必要的。例如在填表單時,有些欄位是必填的,有些是選填的,這種情況下就可以使用可選屬性來表示選填的欄位。
```typescript
interface UserProfile {
name: string;
email: string;
phone?: string; // 電話號碼是可選的
}
```
2. API 回應:從伺服器獲取資料時,不同 API 回應可能會包含不同的屬性,使用可選屬性來處理不同回應之間的差異。
```typescript
interface ApiResponse {
data: any;
error?: string; // 錯誤訊息是可選的
}
```
3. 函示參數:在呼叫函式時,有時候你只需要傳遞部分參數,其他參數則是可選的。這在處理多種使用情況的函式時很有用。
```typescript
function sendMessage(message: string, recipient?: string){
// ..
}
```
4. 配置設定:當你需要配置設定時,某些選項可能是可選的,因為不是每個配置項都需要被指定。
```typescript
interface AppConfig {
theme: string;
language: string;
analyticsEnabled?: boolean; // 分析功能是可選的
}
```
## 任意屬性 Index Signature
在某些情況下,我們可能無法確定物件具有哪些屬性,或者物件可能有動態生成的屬性。這時候,我們會希望一個介面具有任意的屬性。
在 TypeScript 的介面中,除了可選屬性,還有一種特殊的屬性稱為「任意屬性」(Index Signature),它允許你定義物件可以具有的任意屬性。
**使用 `[propName: string]` 來表示任意屬性 (Index Signatures)**:
```typescript
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 也不會報錯。
需注意,**一旦定義了任意屬性,那麼確定屬性和可選屬性的型別都必須是它的型別的子集**。換句話說,任意屬性的型別必須要涵蓋其他屬性的型別才行。
```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` 的子型別。
**一個介面中只能定義一個任意屬性**。
如果想讓任意屬性的屬性值有多個型別,可以**將任意屬性指定為聯合型別**:
```typescript
interface Person {
name: string;
age?: number;
[key: string]: string | number; // 聯合型別,可以是 string 或 number 型別
}
```
### 任意屬性的常見應用
任意屬性在處理動態生成的資料結構時相當有用。
1. **動態屬性**:當物件的屬性名稱不確定時,你可以使用任意屬性來處理這種情況。這在處理不同形式的資料時很有彈性。
```typescript
interface DynamicData {
[key: string]: number;
}
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`** :
```typescript
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` 屬性的值時,編譯器就會報錯,因為該屬性被定義為唯讀,不允許修改:
```typescript
alice.id = 345;
// error TS2540: Cannot assign to 'id' because it is a read-only property.
```
需注意的是,**唯讀的約束是存在於第一次給「物件」賦值的時候,而不是第一次給「唯讀屬性」賦值的時候**:
```typescript
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` 賦值:
```typescript
// index.ts:8:7 - error TS2741: Property 'id' is missing in type '{ name: string; gender: string; }' but required in type 'Person'.
```
第二個是在給 `alice.id` 賦值的時候,由於它是唯讀屬性,所以報錯:
```typescript
// index.ts:13:7 - error TS2540: Cannot assign to 'id' because it is a read-only property.
```
### 唯讀屬性的常見應用
1. **保護物件免於被修改**:有些屬性在物件創建後應該保持不變。使用唯讀屬性可以確保這些值不會被改變,從而防止錯誤。例如:產品的 id 在創建後就不應被修改:
```typescript
interface Product {
readonly id: number;
name: string;
price: number;
}
function updateProduct(product: Product, newName: string) {
// 以下操作會引發編譯錯誤,因為 id 是唯讀屬性
product.id = 123;
product.name = newName;
}
```
2. **可信賴的資料**:當你有一些在運行時設定,但在後續不應該被改變的資料時,可以使用唯讀屬性確保這些資料不被修改。例如當你在程式中設定一些常數或設定值時:
```typescript
interface Config {
readonly apiUrl: string;
readonly maxRequestsPerSecond: number;
}
const config: Config = {
apiUrl: "https://api.example.com",
maxRequestsPerSecond: 10
};
```
3. **共享物件**:在多個地方使用同一個物件時,使用唯讀屬性可以確保這個物件的值不會在其他地方被更改。例如在多個模組或組件中共享配置設定時,使用唯讀屬性可以確保配置不會在其他地方被更改:
```typescript
export interface AppConfiguration {
readonly theme: string;
readonly language: string;
}
const defaultConfig: AppConfiguration = {
theme: "light",
language: "en"
};
// 在其他模組中引入並使用 defaultConfig
```
## Ref
- [TypeScript 新手指南 (gitbook.io)](https://willh.gitbook.io/typescript-tutorial/)
- [從零開始學 TypeScript 計畫](http://anna-yufeng.com/)