TypeScript 真香系列——接口篇


接口帶來了什么好處

好處One —— 過去我們寫 JavaScript

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

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

函數調用:

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

這對於我們之前在寫 JavaScript 的時候,再正常不過了,但是如果這個 getUserInfo 在多人開發過程中,如果它是個公共函數,多個開發者都會調用,如果不是每個人點進來看函數對應注釋,可能會出現以下問題:

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

JavaScript 是弱類型的語言,所以並不會對我們傳入的代碼進行任何的檢測,有些錯你自己都說不清楚,但是就出了問題。

TypeScript 中的 interface 可以解決這個問題

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

 

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

getUserInfo({name: "kaola", 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}); // 錯誤信息:類型不匹配
 

這時候你會發現這段代碼還是有點長,代碼不便與閱讀,這時候就體現了 interface 的必要性。

使用 interface 對 user 的類型進行重構。

我們先定義一個 IUser 接口:

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

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

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

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

// 錯誤的調用和之前一樣,報錯信息也相同不再說明。

接口中函數的定義再次改造

定義兩個接口:

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

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

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

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

// 正確的調用

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

// 錯誤的調用

getUserInfo();
 

好處TWO —— 過去我們用 Node.js 寫后端接口

其實這個說明和上面類似,我再提一下,就是想證明 TypeScript 確實挺香的! 寫一個后端接口,我要特意封裝一個工具類,來檢測前端給我傳遞過來的參數,比如下圖中的validate專門用來檢驗參數的函數

但是有了 TypeScript 這個參數檢驗函數可以省略了,我們可以這樣寫:

 

 const goodParams: IGoodsBody = this.ctx.body;

 

GoodsBody就是對應參數定義的 interface,比如這個樣子

// -- 查詢列表時候使用的接口
interface IQuery {
    page: number;
    rows: number;
    disabledPage?: boolean; // 是否禁用分頁,true將會忽略`page`和`rows`參數
 }
// - 商品
export interface IGoodsQuery extends Query {
    isOnline?: string | number; // 是否出售中的商品
    goodsNo?: string; // 商品編號
    goodsName?: string; // 商品名稱
 }
 
         

接口的基礎篇

接口的定義

和 java 語言相同,TypeScript 中定義接口也是使用 interface 關鍵字來定義:

interface IQuery {
  page: number;
}
 

你會發現我都在接口的前面加了一個I,算是個人習慣吧,之前一直寫 java 代碼,另一方面tslint要求,否則會報一個警告,是否加看個人。

接口中定義方法

看上面的接口中,我們定義了 page 常規屬性,定義接口時候不僅僅可以有 屬性,也可以有方法,看下面的例子:

interface IQuery {
  page: number;
  findOne(): void;
  findAll(): void;
}
 

如果我們有一個對象是該接口類型,那么必須包含對應的屬性和方法(無可選屬性情況):

const q: IQuery = {
  page: 1,
  findOne() {
    console.log("findOne");
  },
  findAll() {
    console.log("findAll");
  },
};
 

接口中定義屬性

普通屬性

上面的 page 就是普通屬性,如果有一個對象是該接口類型,那么必須包含對應的普通屬性。就不具體說了。

可選屬性

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

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

interface IQuery {
  page: number;
  findOne(): void;
  findAll(): void;
  isOnline?: string | number; // 是否出售中的商品
  delete?(): void
}
 

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

注意:可選屬性如果沒有賦值,那么獲取到的值是undefined; 對於可選方法,必須先進行判斷,再調用,否則會報錯;

const q: IQuery = {
 page: 1,
 findOne() {
   console.log("findOne");
 },
 findAll() {
   console.log("findAll");
 },
};

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

正確的調用方式如下:

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

 

大家可能會問既然是可選屬性,可有可無的,那么為什么還要定義呢?對比起完全不定義,定義可選屬性主要是:為了讓接口更加的靈活,某些屬性我們可能希望設計成可選,並且如果存在屬性,能約束類型,而這也是十分關鍵的。

只讀屬性

默認情況下,接口中定義的屬性可讀可寫: 但是有一個關鍵字 readonly,定義的屬性值,不可以進行修改,強制修改后報錯。

interface IQuery {
  readonly page: number;
  findOne(): void;
}
 

page屬性加了readonly關鍵字,再給它賦值會報錯。

const q: IQuery = {
  page: 1,
  findOne() {
    console.log("findOne");
  },
};
q.page = 10;// Cannot assign to 'page' because it is a read-only property.
 

接口的高級篇

函數類型接口

Interface 還可以用來規范函數的形狀。Interface 里面需要列出參數列表返回值類型的函數定義。寫法如下:

  • 定義了一個函數接口
  • 接口接收三個參數並且不返回任何值
  • 使用函數表達式來定義這種形狀的函數
interface Func {
    // ✔️ 定於這個函數接收兩個必選參數都是 number 類型,以及一個可選的字符串參數 desc,這個函數不返回任何值
    (x: number, y: number, desc?: string): void
}

const sum: Func = function (x, y, desc = '') {
    // const sum: Func = function (x: number, y: number, desc: string): void {
    // ts類型系統默認推論可以不必書寫上述類型定義
    console.log(desc, x + y)
}

sum(32, 22)
 

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

type Func = (x: number, y: number, desc?: string) => void;
 

接口的實現

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

下面的代碼中會有關於修飾符的警告,暫時忽略,后面詳細講解 // 定義一個實體接口

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 來繼承。

interface Shape {     //定義接口Shape
    color: string;
}

interface Square extends Shape {  //繼承接口Shape
    sideLength: number;
}
 

一個 interface 可以同時繼承多個 interface ,實現多個接口成員的合並。用逗號隔開要繼承的接口。

interface Shape {
    color: string;
}

interface PenStroke {
    penWidth: number;
}

interface Square extends Shape, PenStroke {
    sideLength: number;
}
 

  需要注意的是,盡管支持繼承多個接口,但是如果繼承的接口中,定義的同名屬性的類型不同的話,是不能編譯通過的。如下代碼:

interface Shape {
    color: string;
    test: number;
}

interface PenStroke extends Shape{
    penWidth: number;
    test: string;
}
 

另外關於繼承還有一點,如果現在有一個類實現了 Square 接口,那么不僅僅需要實現 Square 的方法,也需要實現 Square 繼承自的接口中的方法,實現接口使用 implements 關鍵字 。

可索引類型接口

interface和type的區別

type 可以而 interface 不行

  • type 可以聲明基本類型別名,聯合類型,元組等類型
// 基本類型別名
type Name = string

// 聯合類型
interface Dog {
    wong();
}
interface Cat {
    miao();
}

type Pet = Dog | Cat

// 具體定義數組每個位置的類型
type PetList = [Dog, Pet]

 

  • type 語句中還可以使用 typeof 獲取實例的 類型進行賦值
// 當你想獲取一個變量的類型時,使用 typeof

let div = document.createElement('div');
type B = typeof div
  • type 其他騷操作
type StringOrNumber = string | number;  
type Text = string | { text: string };  
type NameLookup = Dictionary<string, Person>;  
type Callback<T> = (data: T) => void;  
type Pair<T> = [T, T];  
type Coordinates = Pair<number>;  
type Tree<T> = T | { left: Tree<T>, right: Tree<T> };

interface 可以而 type 不行

interface 能夠聲明合並

interface User {
  name: string
  age: number
}

interface User {
  sex: string
}

/*
User 接口為 {
  name: string
  age: number
  sex: string 
}
*/
 

另外關於type的更多內容,可以查看文檔:TypeScript官方文檔

接口的應用場景總結

在項目中究竟怎么用,開篇已經舉了兩個例子,在這里再簡單寫一點,最近嘗試了一下egg+ts,學習下。在寫查詢參數檢驗的時候,或者返回固定數據的時候,都會用到接口,看一段簡單代碼,已經看完了上面的文章,自己體會下吧。

import User from '../model/user';
import Good from '../model/good';

// 定義基本查詢類型
// -- 查詢列表時候使用的接口
interface Query {
    page: number;
    rows: number;
    disabledPage?: boolean; // 是否禁用分頁,true將會忽略`page`和`rows`參數
  }

// 定義基本返回類型
type GoodResult<Entity> = {
    list: Entity[];
    total: number;
    [propName: string]: any;
};

// - 商品
export interface GoodsQuery extends Query {
    isOnline?: string | number; // 是否出售中的商品
    goodsNo?: string; // 商品編號
    goodsName?: string; // 商品名稱
}
export type GoodResult = QueryResult<Good>;


作者:ikoala
鏈接:https://juejin.im/post/5dd1098e51882529f21587db
來源:掘金
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM