TypeScript(四)接口interface使用詳解

一. 爲什麼要使用接口

1.1. JavaScript存在的問題

我們在JavaScript中定義一個函數,用於獲取一個用戶的姓名和年齡的字符串:

const getUserInfo = function(user) {
  return `name: ${user.name}, age: ${user.age}`
}

正確的調用方法應該是下面的方式:

getUserInfo({name: "coderwhy", age: 18})

但是當項目比較大,或者多人開發時,會出現錯誤的調用方法:

// 錯誤的調用
getUserInfo() // Uncaught TypeError: Cannot read property 'name' of undefined
console.log(getUserInfo({name: "coderwhy"})) // name: coderwhy, age: undefined
getUserInfo({name: "codewhy", height: 1.88}) // name: coderwhy, age: undefined

因爲JavaScript是弱類型的語言,所以並不會對我們傳入的代碼進行任何的檢測,但是在之前的javaScript中確確實實會存在很多類似的安全隱患。

如何避免這樣的問題呢?

  • 當然是使用TypeScript來對代碼進行重構

1.2. TypeScript代碼重構一

我們可以使用TypeScript來對上面的代碼進行改進:

const getUserInfo = (user: {name: string, age: number}): string => {
  return `name: ${user.name} age: ${user.age}`;
};

正確的調用是如下的方式:

getUserInfo({name: "coderwhy", age: 18});

如果調用者出現了錯誤的調用,那麼TypeScript會直接給出錯誤的提示信息:

// 錯誤的調用
getUserInfo(); // 錯誤信息:An argument for 'user' was not provided.
getUserInfo({name: "coderwhy"}); // 錯誤信息:Property 'age' is missing in type '{ name: string; }'
getUserInfo({name: "coderwhy", height: 1.88}); // 錯誤信息:類型不匹配

這樣確實可以防止出現錯誤的調用,但是我們在定義函數的時候,參數的類型和函數的類型都是非常長的,代碼非常不便於閱讀

所以,我們可以使用接口來對代碼再次進行重構。

1.3. TypeScript代碼重構二

現在我們使用接口來對user的類型進行重構。

接口重構一:參數類型使用接口定義

我們先定義一個IUser接口:

// 先定義一個接口
interface IUser {
  name: string;
  age: number;
}

接下來我們看一下函數如何來寫:

const getUserInfo = (user: IUser): string => {
  return `name: ${user.name}, age: ${user.age}`;
};

// 正確的調用
getUserInfo({name: "coderwhy", age: 18});

// 錯誤的調用,其他也是一樣
getUserInfo();

接口重構二:函數的類型使用接口定義好(後面會詳細講解接口函數的定義)

我們先定義兩個接口:

  • 第二個接口定義有一個警告,我們暫時忽略它,它的目的是如果一個函數接口只有一個方法,那麼可以使用type來定義

  • type IUserInfoFunc = (user: IUser) => string;

interface IUser {
  name: string;
  age: number;
}

interface IUserInfoFunc {
  (user: IUser): string;
}

接着我們去定義函數和調用函數即可:

const getUserInfo: IUserInfoFunc = (user) => {
  return `name: ${user.name}, age: ${user.age}`;
};

// 正確的調用
getUserInfo({name: "coderwhy", age: 18});

// 錯誤的調用
getUserInfo();

二. 接口的基本使用

2.1. 接口的定義方式

和其他很多的語言類似,TypeScript中定義接口也是使用interface關鍵字來定義:

interface IPerson {
  name: string;
}

你會發現我都在接口的前面加了一個I,這是tslint要求的,否則會報一個警告

要不要加前綴是根據公司規範和個人習慣

interface name must start with a capitalized I

當然我們可以在tslint中關閉掉它:在rules中添加如下規則

"interface-name" : [true, "never-prefix"]

2.2. 接口中定義方法

定義接口中不僅僅可以有屬性,也可以有方法:

interface Person {
  name: string;
  run(): void;
  eat(): void;
}

如果我們有一個對象是該接口類型,那麼必須包含對應的屬性和方法:

const p: Person = {
  name: "why",
  run() {
    console.log("running");
  },
  eat() {
    console.log("eating");
  },
};

2.3. 可選屬性的定義

默認情況下一個變量(對象)是對應的接口類型,那麼這個變量(對象)必須實現接口中所有的屬性和方法。

但是,開發中爲了讓接口更加的靈活,某些屬性我們可能希望設計成可選的(想實現可以實現,不想實現也沒有關係),這個時候就可以使用可選屬性(後面詳細講解函數時,也會講到函數中有可選參數):

interface Person {
  name: string;
  age?: number;
  run(): void;
  eat(): void;
  study?(): void;
}

上面的代碼中,我們增加了age屬性和study方法,這兩個都是可選的:

  • 可選屬性如果沒有賦值,那麼獲取到的值是undefined;

  • 對於可選方法,必須先進行判斷,再調用,否則會報錯;

const p: Person = {
  name: "why",
  run() {
    console.log("running");
  },
  eat() {
    console.log("eating");
  },
};

console.log(p.age); // undefined
p.study(); // 不能調用可能是“未定義”的對象。

正確的調用方式如下:

if (p.study) {
  p.study();
}

2.4. 只讀屬性的定義

默認情況下,接口中定義的屬性可讀可寫:

console.log(p.name);
p.name = "流川楓";

如果一個屬性,我們只是希望在定義的時候就定義值,之後不可以修改,那麼可以在屬性的前面加上一個關鍵字:readonly

interface Person {
  readonly name: string;
  age?: number;
  run(): void;
  eat(): void;
  study?(): void;
}

當我在name前面加上readonly時,賦值語句就會報錯:

console.log(p.name);
p.name = "流川楓"; // Cannot assign to 'name' because it is a read-only property.

三. 接口的高級使用

3.1. 函數類型的定義

接口不僅僅可以定義普通的對象類型,也可以定義函數的類型

// 函數類型的定義
interface SumFunc {
  (num1: number, num2: number): number;
}

// 定義具體的函數
const sum: SumFunc = (num1, num2) => {
  return num1 + num2;
};

// 調用函數
console.log(sum(20, 30));

不過上面的接口中只有一個函數,TypeScript會給我們一個建議,可以使用type來定義一個函數的類型:

type SumFunc = (num1: number, num2: number) => number;

關於type的更多用戶,我們後面專門進行講解,暫時不在接口中展開討論。

3.2. 可索引類型的定義

和使用接口描述函數的類型差不多,我們也可以使用接口來描述 可索引類型

  • 比如一個變量可以這樣訪問:a[3],a["name"]

可索引類型具有一個 索引簽名,它描述了對象索引的類型,還有相應的索引返回值類型。

// 定義可索引類型的接口
interface RoleMap {
  [index: number]: string;
}

// 賦值具體的值
// 賦值方式一:
const roleMap1: RoleMap = {
  0: "學生",
  1: "講師",
  2: "班主任",
};

// 賦值方式二:因爲數組本身是可索引的值
const roleMap2 = ["魯班七號", "露娜", "李白"];

// 取出對應的值
console.log(roleMap1[0]); // 學生
console.log(roleMap2[1]); // 露娜

上面的案例中,我們的索引簽名是數字類型, TypeScript支持兩種索引簽名:字符串和數字

我們來定義一個字符串的索引類型:

interface RoleMap {
  [name: string]: string;
}

const roleMap: RoleMap = {
  aaa: "魯班七號",
  bbb: "露娜",
  ccc: "李白",
};

console.log(roleMap.aaa);
console.log(roleMap["aaa"]); // 警告:不推薦這樣來取

可以同時使用兩種類型的索引,但是數字索引的返回值必須是字符串索引返回值類型的子類型:

  • 這是因爲當使用 number來索引時,JavaScript會將它轉換成string然後再去索引對象。

class Person {
  private name: string = "";
}

class Student extends Person {
  private sno: number = 0;
}

// 下面的代碼會報錯
interface IndexSubject {
  [index: number]: Person;
  [name: string]: Student;
}

代碼會報如下錯誤:

數字索引類型“Person”不能賦給字符串索引類型“Student”。

修改爲如下代碼就可以了:

interface IndexSubject {
  [index: number]: Student;
  [name: string]: Person;
}

下面的代碼也會報錯:

  • letter索引得到結果的類型,必須是Person類型或者它的子類型

interface IndexSubject {
  [index: number]: Student;
  [name: string]: Person;

  letter: string;
}

3.3. 接口的實現

接口除了定義某種類型規範之後,也可以和其他編程語言一樣,讓一個類去實現某個接口,那麼這個類就必須明確去擁有這個接口中的屬性和實現其方法:

 

從代碼設計上,爲什麼需要接口?

 

當然,對於初次接觸接口的人,還是很難理解它在實際的代碼設計中的好處,這點慢慢體會,不用心急。

3.3. 接口的繼承

和類相似(後面我們再詳細學習類的知識),接口也是可以繼承接口來提供複用性:

 

  • 下面的代碼中會有關於修飾符的警告,暫時忽略,後面詳細講解

    // 定義一個實體接口
    interface Entity {
      title: string;
      log(): void;
    }
    
    // 實現這樣一個接口
    class Post implements Entity {
      title: string;
    
      constructor(title: string) {
        this.title = title;
      }
    
      log(): void {
        console.log(this.title);
      }
    }
    

    思考:我定義了一個接口,但是我在繼承這個接口的類中還要寫接口的實現方法,那我不如直接就在這個類中寫實現方法豈不是更便捷,還省去了定義接口?這是一個初學者經常會有疑惑的地方。

    從思考方式上,爲什麼需要接口?

  • 我們從生活出發理解接口

  • 比如你去三亞/杭州旅遊, 玩了一上午後飢餓難耐, 你放眼望去, 會注意什麼? 飯店!!

  • 你可能並不會太在意這家飯店叫什麼名字, 但是你知道只要後面有飯店兩個字, 就意味着這個地方必然有飯店的實現 – 做各種菜給你喫;

  • 接口就好比飯店/酒店/棋牌室這些名詞後面添加的附屬詞, 當我們看到這些附屬詞後就知道它們具備的功能

  • 在代碼設計中,接口是一種規範;

  • 注意:繼承使用extends關鍵字

    • 接口通常用於來定義某種規範, 類似於你必須遵守的協議, 有些語言直接就叫protocol;

    • 站在程序角度上說接口只規定了類裏必須提供的屬性和方法,從而分離了規範和實現,增強了系統的可拓展性和可維護性; 

      interface Barkable {
        barking(): void;
      }
      
      interface Shakable {
        shaking(): void;
      }
      
      interface Petable extends Barkable, Shakable {
        eating(): void;
      }

      接口Petable繼承自Barkable和Shakable,另外我們發現一個接口可以同時繼承自多個接口

      如果現在有一個類實現了Petable接口,那麼不僅僅需要實現Petable的方法,也需要實現Petable繼承自的接口中的方法:

    • 注意:實現接口使用implements關鍵字

      class Dog implements Petable {
        barking(): void {
          console.log("汪汪叫");
        }
      
        shaking(): void {
          console.log("搖尾巴");
        }
      
        eating(): void {
          console.log("喫骨頭");
        }
      }

       

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章