TypeScript - 物件的型別:介面 Interface

·

4 min read

什麼是介面(Interface)?

在物件導向程式語言中,介面(Interfaces)是一個很重要的概念。它用來定義物件的結構和行為。一個介面描述了一個物件應該有哪些屬性、方法以及其型別,類似於一個「契約」,告訴你的程式碼應該如何組織資料。

TypeScript 中的介面是一個非常有彈性的概念,不僅可以用來「對類別(Class)的一部分行為進行抽象」也常用來對「物件的形狀(Shape)進行描述」。

  1. 對類別的行為進行抽象:定義一個類別的行為和契約,即該類別需要實現特定的方法或行為。這樣的好處是可以確保不同的類別都具有相同的方法,讓程式碼更具可讀性和可維護性。

  2. 對物件的形狀進行描述:介面也可以用來描述物件的外觀和結構,即物件應擁有哪些屬性和方法。這種情況下,我們通常稱介面為「形狀(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'
}

可選屬性的常見應用

可選屬性在處理物件的時候很有彈性。特別是當你在處理不完整的資料,或是不同物件間的共享屬性時非常有用。

  1. 用戶資訊、填寫表單:當我們在獲取用戶資訊時,並不是所有資料都是必要的。例如在填表單時,有些欄位是必填的,有些是選填的,這種情況下就可以使用可選屬性來表示選填的欄位。

     interface UserProfile {
         name: string;
         email: string;
         phone?: string; // 電話號碼是可選的
     }
    
  2. API 回應:從伺服器獲取資料時,不同 API 回應可能會包含不同的屬性,使用可選屬性來處理不同回應之間的差異。

     interface ApiResponse {
         data: any;
         error?: string; // 錯誤訊息是可選的
     }
    
  3. 函示參數:在呼叫函式時,有時候你只需要傳遞部分參數,其他參數則是可選的。這在處理多種使用情況的函式時很有用。

     function sendMessage(message: string, recipient?: string){
         // ..
     }
    
  4. 配置設定:當你需要配置設定時,某些選項可能是可選的,因為不是每個配置項都需要被指定。

     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 型別
}

任意屬性的常見應用

任意屬性在處理動態生成的資料結構時相當有用。

  1. 動態屬性:當物件的屬性名稱不確定時,你可以使用任意屬性來處理這種情況。這在處理不同形式的資料時很有彈性。

    ```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 屬性的值就不能再被修改。

當我們嘗試修改 aliceid 屬性的值時,編譯器就會報錯,因為該屬性被定義為唯讀,不允許修改:

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.

唯讀屬性的常見應用

  1. 保護物件免於被修改:有些屬性在物件創建後應該保持不變。使用唯讀屬性可以確保這些值不會被改變,從而防止錯誤。例如:產品的 id 在創建後就不應被修改:

     interface Product {
         readonly id: number;
         name: string;
         price: number;
     }
    
     function updateProduct(product: Product, newName: string) {
         // 以下操作會引發編譯錯誤,因為 id 是唯讀屬性
         product.id = 123;
         product.name = newName;
     }
    
  2. 可信賴的資料:當你有一些在運行時設定,但在後續不應該被改變的資料時,可以使用唯讀屬性確保這些資料不被修改。例如當你在程式中設定一些常數或設定值時:

     interface Config {
         readonly apiUrl: string;
         readonly maxRequestsPerSecond: number;
     }
    
     const config: Config = {
         apiUrl: "https://api.example.com",
         maxRequestsPerSecond: 10
     };
    
  3. 共享物件:在多個地方使用同一個物件時,使用唯讀屬性可以確保這個物件的值不會在其他地方被更改。例如在多個模組或組件中共享配置設定時,使用唯讀屬性可以確保配置不會在其他地方被更改:

     export interface AppConfiguration {
         readonly theme: string;
         readonly language: string;
     }
    
     const defaultConfig: AppConfiguration = {
         theme: "light",
         language: "en"
     };
    
     // 在其他模組中引入並使用 defaultConfig
    

Ref