TypeScript-接口的使用

前言

眾所周知,在傳統(tǒng)的JavaScript中是沒有接口的概念的,所謂的接口,其實(shí)就是描述集合屬性的類型的一個(gè)特殊的虛擬結(jié)構(gòu)。這也是開發(fā)一個(gè)大型項(xiàng)目所必須的語言特性,像Java、C#這樣強(qiáng)類型語言,接口已經(jīng)使用得非常廣泛。于是,在TypeScrip中也引入了接口的概念。

一、接口的基本使用

基與我們前面介紹的對(duì)象的類型的聲明,可以定義一個(gè)函數(shù)的參數(shù)是包含特定屬性的對(duì)象:

function doSomeThing(params: {name: string}):void {
  console.log(params); 
}

console.log(doSomeThing({name: '馬松松'}));

// { name: '馬松松' }

我們也可以使用接口的方式實(shí)現(xiàn)上面的例子:

interface person {
  name: string
}
function doSomeThing(params: person):void {
  console.log(params); 
}

console.log(doSomeThing({name: '馬松松'}));

// { name: '馬松松' }

兩者是等效的,使用接口的好處是可以將參數(shù)類型的配置抽離到一個(gè)單獨(dú)的文件,這樣使得項(xiàng)目更容易維護(hù)。

二、接口中使用可選參數(shù)

為了增強(qiáng)接口的靈活性和延展性,TypeScript允許定義為接口類型的變量可以選擇性匹配。

interface SquireParams {
  width?: number,
  height?: number
}

function squireResult(params: SquireParams):any {
  let result: any;
  if (params.width) {
    result = params.width * params.width;
  }
  if (params.height) {
    result = params.height * params.height;
  }
  if (params.width && params.height) {
    result = params.width * params.height;
  }

  return result;
}

console.log(squireResult({height: 5}));
// 25

console.log(squireResult({width: 5}));
// 25

console.log(squireResult({width: 5,height: 5}));
// 25

當(dāng)然,也可以和必選參數(shù)結(jié)合使用:

interface SquireParams {
  width?: number,
  height?: number,
  label: string
}

function squireResult(params: SquireParams):any {
  let result: any;
  if (params.width) {
    result = params.label +  params.width * params.width;
  }
  if (params.height) {
    result = params.label + params.height * params.height;
  }
  if (params.width && params.height) {
    result = params.label + params.width * params.height;
  }

  return result;
}

console.log(squireResult({label: '計(jì)算結(jié)果為:', height: 5}));
// 計(jì)算結(jié)果為:25

三、接口中使用 只讀屬性

同時(shí),在JavaScript中,沒有關(guān)鍵字標(biāo)識(shí)只讀屬性。我們可以通過Object.defineProperty屬性設(shè)置攔截,在TypeScript中明確提出了只讀屬性的關(guān)鍵字。

可以這樣使用:

interface readonlyType {
  readonly x: number,
  readonly y: number
}

let readonlyObj: readonlyType = {x: 10, y: 10}
readonlyObj.x = 13;

//Cannot assign to 'x' because it is a read-only property 

只允許初始化的時(shí)候,給xy分配number的值。

對(duì)于數(shù)組,TypeScript也提供了ReadonlyArray<T>這樣的泛型只讀數(shù)組,刪除了該命名數(shù)組的操作數(shù)組的所有方法。

const arr: ReadonlyArray<number> = [1,2,3];

當(dāng)你想往該數(shù)組推入一個(gè)數(shù)字時(shí),會(huì)引發(fā)錯(cuò)誤:

arr.push()
// Property 'push' does not exist on type 'readonly number[]'

??對(duì)于const和readonly的使用的場(chǎng)景:

TypeScript的官方推薦是:變量使用const,而屬性使用readonly。

四、Excess Property Checks

這個(gè)是解決原生的JavaScript的行為和TypeScript行為不一致的方案,思考這樣一個(gè)例子:

interface SquareConfig {
  color ?: string,
  width ?: number
}

function createSquare(config: SquareConfig): { color: string; area: number } {
  return { color: config.color || "red", area: config.width ? config.width * config.width : 20 };
}

我們定義了一個(gè)SquareConfig接口,然后作為函數(shù)的入?yún)㈩愋?,然后我們這樣使用這個(gè)函數(shù):

let mySquare = createSquare({ colour: "red", width: 100 });

這里TypeScript會(huì)給出錯(cuò)誤提示類型不匹配,但是按照我們之前說的可選參數(shù)的的例子,這里的color并不是必須的,因?yàn)檫@里故意將color拼成了colour,TypeScript對(duì)以字面量方式定義對(duì)象的方式進(jìn)行了特殊的類型檢查處理,而在原生的JavaScript中是靜默忽略的,為了避免出現(xiàn)這種情況,下面是幾種更好的規(guī)避這種錯(cuò)誤的方式:

1.使用as 強(qiáng)制推斷類型

let mySquare = createSquare({colour: "red", width: 100} as SquareConfig);

2.不使用字面量的方式

let paramsSquare = {colour: "red", width: 100};
let mySecondSquare = createSquare(paramsSquare);

3.加一個(gè)額外的動(dòng)態(tài)屬性

interface SquareConfig {
  color ?: string,
  width ?: number,
  [propName: string]: any;
}

let myThirdSquare = createSquare({colour: "red", width: 100});

當(dāng)你想用傳字面量的方式傳入?yún)?shù),為了規(guī)避不必要的錯(cuò)誤,使用上面的幾種方式就行。

五、在接口中定義 函數(shù)的參數(shù)類型和返回值類型

1.基本使用:

首先定義一個(gè)函數(shù)的接口,我們定義了參數(shù)的類型和返回值的類型

interface baseFunc {
  (firstName: string, lastName: string): string
}

然后這樣使用這個(gè)接口:

let myFunc: baseFunc  = function (firstName: string, lastName: string) {
  return firstName + lastName;
}

2.函數(shù)的入?yún)⒉恍枰?/p>

let mySecondFunc: baseFunc  = function (fName: string, lName: string) {
  return fName + lName;
}

3.當(dāng)你指定了函數(shù)簽名的類型 函數(shù)的入?yún)⒑头祷刂狄膊恍枰该黝愋?,類型系統(tǒng)會(huì)自動(dòng)根據(jù)傳入的參數(shù)推斷類型

let myThirdFunc: baseFunc  = function (fName, lName) {
  return fName + lName;
}

4.但是如果你沒有指定類型 但是返回了和接口返回類型不一致 類型檢查不會(huì)通過

let myLastFunc: baseFunc  = function (fName, lName) {
  let result =  fName + lName;

  return 11;
}

六、接口中 定義數(shù)組和對(duì)象的索引類型

1.基本使用:

interface objectType {
  [index: string]: string;
}

在對(duì)象中這樣使用這個(gè)接口

let myObj: objectType = {name: '馬松松', age: "18"}; 

可以看到,我們定義的索引是string,屬性值也是string,所以這樣定義是合理的。

但是如果將age的屬性定義為number類型,就不符合我們接口的定義:

let myObj: objectType = {name: '馬松松', age: 18}; // 這樣是不符合接口的定義的

在數(shù)組中需要這樣使用定義接口,數(shù)組的索引都是number類型的:

interface arrayType {
  [index: number]: string;
}

然后,你可以這樣使用這個(gè)接口:

let myArr: arrayType = ["馬松松","18"];

2.注意字符串索引和直接指定類型的方式一起使用的時(shí)候,字符串索引類型的優(yōu)先級(jí)更高,所以直接指明屬性的類型 需要保持和字符串索引一樣.

interface numberDictionary {
  [index: string]: number,
  length: number,
  // name: string // 這里使用string會(huì)報(bào)錯(cuò),以為你字符串索引返回的類型是number
  name: number, // 這樣是可以的
}

3.那你確實(shí)想定義不同類型的屬性 可以這樣做

interface secondNumberDictionary {
  [index: string]: number | string,
  length: number,
  name: string // 這樣是可以的
}

4.也可以結(jié)合 readonly 定義只讀屬性

interface thirdNumberDistionary {
  readonly [index: string]: string
}

// 此時(shí)當(dāng)你想設(shè)置thirdNumberDistionary的屬性的時(shí)候就會(huì)報(bào)錯(cuò)
let myThirdNumberDictionary: thirdNumberDistionary = {name: '馬松松'};
// myThirdNumberDictionary.name = "宋志露"; // 不可設(shè)置

七、類和接口的關(guān)系

其他語言中使用接口做頻繁的操作的就是,用類實(shí)現(xiàn)一個(gè)接口,從而使得類和接口締結(jié)某種強(qiáng)制的聯(lián)系。

1.基本使用:

我們首先定義一個(gè)日期接口:

interface BaseClock {
  currentTime: string
}

使用implements關(guān)鍵詞締結(jié)類和接口的契約關(guān)系:

class MyClock implements BaseClock {
  currentTime: ""
  constructor(h: number, m: number) {

  }
}

締結(jié)的契約關(guān)系為:MyClock類中必須有類型為stringcurrentTime變量。

2.也可以締結(jié)類中的方法的契約

先定義接口:

interface SecondBaseClock {
  getCurrentTime(t: string): void
}

使用implements締結(jié)契約:

class MySecondClock implements SecondBaseClock {
  getCurrentTime(t: string) {
    this.currentTime = t;
  }
}

締結(jié)的契約關(guān)系為:MySecondClock類中需要有一個(gè)getCurrentTime方法,且需要一個(gè)類型為string的入?yún)?,沒有返回值。

3.在締結(jié)類和接口的契約關(guān)系時(shí) 注意new關(guān)鍵詞

當(dāng)使用new關(guān)鍵詞實(shí)例化類時(shí),TypeScript類型檢查器不會(huì)檢查靜態(tài)類的構(gòu)造器方法是否滿足締約,而是在你使用new關(guān)鍵詞的時(shí)候判斷是否滿足。

比如我們定義一個(gè)構(gòu)造函數(shù)的的接口:

interface C {
  new (hour: number, min: number)
}

然后使用implements締結(jié)契約:

class Clock implements C {
  constructor(h: number, m: number) {}
}

我們會(huì)得到報(bào)錯(cuò)信息:

Class 'Clock' incorrectly implements interface 'C'.Type 'Clock' provides no match for the signature 'new (hour: number, min: number): any'

我們締結(jié)契約的類實(shí)際上是滿足了構(gòu)造函數(shù)的接口的,但是由于TypeScript類型檢查不會(huì)直接檢查類中構(gòu)造函數(shù)是否滿足契約,所以這里會(huì)報(bào)錯(cuò)。

所以正確的使用方式是將締結(jié)契約的類賦值給別的變量,這樣類型檢查系統(tǒng)就會(huì)進(jìn)行類型檢查:

interface ClockInterface {
  tick(): void
}
const Clock: ClockConstructor = class Clock implements ClockInterface {
  constructor(h: number, m: number) {}
  tick() {

  }
}

這里注意這樣的區(qū)別就好了。

八、接口中 使用繼承

1.基本使用

我們首先定義一個(gè)Square接口:

interface Square {
  width: number,
  height: number
}

然后這樣使用:

let square = {} as Square;
square.width = 100;
square.height = 100;

為了使接口可以更靈活的構(gòu)建更復(fù)雜的數(shù)據(jù)結(jié)構(gòu),這里使用到了extends關(guān)鍵字:

interface baseSquare {
  width: number,
}

interface Square extends baseSquare {
  height: number
}
let square = {} as Square;
square.width = 100;
square.height = 100;

2.一個(gè)接口可以繼承多個(gè)接口

interface baseFirstSquare {
  width: number,
}
interface baseSecondSquare {
  width: number,
}

然后我們可以同時(shí)繼承這樣兩個(gè)接口:

class MySquare implements baseFirstSarare,baseSecondSquare {
  color: string
}

九、接口中使用 混合類型

基于JavaScript語言的豐富性和靈活性,TypeScript允許使用混合類型

比如定義一個(gè)定時(shí)器接口:

interface Counter {
  (start: number): string;
  interval: number;
  reset(): void;
}

然后你這樣使用:

function getCounter(): Counter {
  let counter = function (start: number) {} as Counter;
  counter.interval = 123;
  counter.reset = function () {};
  return counter;
}

let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;

這里使用as推斷了類型,就獲取了一個(gè)對(duì)象,這里個(gè)人有點(diǎn)不理解。

九、接口繼承

1.當(dāng)繼承的類是public時(shí),可以直接實(shí)現(xiàn)這個(gè)接口

class Control {
  public state: any
}

interface SelectableControl extends Control {
  select(): void
}

這樣使用:

let select: SelectableControl = {
  state: 22,
  select() {}
}

2.當(dāng)繼承的類private或者protected時(shí) 繼承的接口只能通過被繼承類子類去實(shí)現(xiàn),不能直接實(shí)現(xiàn)

class SecondControl {
  private state: any
}

interface SecondSelectableControl extends SecondControl {
  select(): void
}

只能是被繼承類的子類去實(shí)現(xiàn)該接口,因?yàn)橹挥斜焕^承類的子類才能訪問私有屬性:

class MySecondSelectableControl extends SecondControl implements SecondSelectableControl {
  select() {

  }
}

然后你這樣使用:

let s = new MySecondSelectableControl();

總結(jié):接口的使用,其實(shí)也是引進(jìn)了強(qiáng)類型語言的相關(guān)的概念,理解接口概念的同時(shí),同時(shí)也能增強(qiáng)前端開發(fā)者對(duì)強(qiáng)類型語言和弱類型語言的特性的理解。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容