TypeScript 接口(Interface)
本節(jié)介紹 TypeScript 各種類型接口的聲明及其使用方法,接口在 TypeScript 中是極其重要的,我們使用接口來(lái)定義契約,如類型命名、屬性檢查、函數(shù)類型定義等。
在下一節(jié)學(xué)習(xí)完類之后,你會(huì)知道類也可以作為接口來(lái)使用。接口的種類繁多,在學(xué)習(xí)過(guò)程中一定要親手編寫(xiě),以達(dá)到靈活使用。
1. 慕課解釋
TypeScript 的核心原則之一是對(duì)值所具有的結(jié)構(gòu)進(jìn)行類型檢查。 它有時(shí)被稱做“鴨式辨型法”或“結(jié)構(gòu)性子類型化”。 在 TypeScript 里,接口的作用就是為這些類型命名和為你的代碼或第三方代碼定義契約?!俜蕉x
接口是對(duì) JavaScript 本身的隨意性進(jìn)行約束,通過(guò)定義一個(gè)接口,約定了變量、類、函數(shù)等應(yīng)該按照什么樣的格式進(jìn)行聲明,實(shí)現(xiàn)多人合作的一致性。TypeScript 編譯器依賴接口用于類型檢查,最終編譯為 JavaScript 后,接口將會(huì)被移除。
// 語(yǔ)法格式
interface DemoInterface {
}
2. 應(yīng)用場(chǎng)景
在聲明一個(gè)對(duì)象、函數(shù)或者類時(shí),先定義接口,確保其數(shù)據(jù)結(jié)構(gòu)的一致性。
在多人協(xié)作時(shí),定義接口尤為重要。
3. 接口的好處
過(guò)去我們寫(xiě) JavaScript 定義一個(gè)函數(shù):
function getClothesInfo(clothes) {
console.log(clothes.price)
}
let myClothes = {
color: 'black',
size: 'XL',
price: 98
}
getClothesInfo(myClothes)
之前我們寫(xiě) JavaScript 這樣是很正常的,但同時(shí)你可能會(huì)遇到下面這些問(wèn)題:
getClothesInfo() // Uncaught TypeError: Cannot read property 'price' of undefined
getClothesInfo({ color: 'black' }) // undefined
相信原因你也知道,JavaScript 是 弱類型
語(yǔ)言,并不會(huì)對(duì)傳入的參數(shù)進(jìn)行任何檢測(cè),錯(cuò)誤在運(yùn)行時(shí)才被發(fā)現(xiàn)。那么通過(guò)定義 接口
,在編譯階段甚至開(kāi)發(fā)階段就避免掉這類錯(cuò)誤,接口將檢查類型是否和某種結(jié)構(gòu)做匹配。
3.1 舉例說(shuō)明
下面通過(guò)接口的方式重寫(xiě)之前的例子:
interface Clothes {
color: string;
size: string;
price: number;
}
function getClothesInfo(clothes: Clothes) {
console.log(clothes.price)
}
let myClothes: Clothes = {
color: 'black',
size: 'XL',
price: 98
}
getClothesInfo(myClothes)
代碼解釋: 代碼中,定義了一個(gè)接口 Clothes
,在傳入的變量 clothes
中,它的類型為 Clothes
。這樣,就約束了這個(gè)傳入對(duì)象的 外形
與接口定義一致。只要傳入的對(duì)象滿足上面的類型約束,那么它就是被允許的。
Tips:
-
定義接口要
首字母大寫(xiě)
。 -
只需要關(guān)注值的
外形
,并不像其他語(yǔ)言一樣,定義接口是為了實(shí)現(xiàn)。 -
如果沒(méi)有特殊聲明,定義的變量比接口少了一些屬性是不允許的,多一些屬性也是不允許的,賦值的時(shí)候,變量的形狀必須和接口的形狀保持一致。
4. 接口的屬性
4.1 可選屬性
接口中的屬性不全是必需的。可選屬性的含義是該屬性在被變量定義時(shí)可以不存在。
// 語(yǔ)法
interface Clothes {
color?: string;
size: string;
price: number;
}
// 這里可以不定義屬性 color
let myClothes: Clothes = {
size: 'XL',
price: 98
}
帶有可選屬性的接口與普通的接口定義差不多,只是在可選屬性名字定義的后面加一個(gè) ?
符號(hào)。
這時(shí),仍不允許添加未定義的屬性,如果引用了不存在的屬性時(shí) TS 將直接捕獲錯(cuò)誤。
4.2 只讀屬性
一些對(duì)象屬性只能在對(duì)象剛剛創(chuàng)建的時(shí)候修改其值。你可以在屬性名前用 readonly
來(lái)指定只讀屬性,比如價(jià)格是不能被修改的:
// 語(yǔ)法
interface Clothes {
color?: string;
size: string;
readonly price: number;
}
// 創(chuàng)建的時(shí)候給 price 賦值
let myClothes: Clothes = {
size: 'XL',
price: 98
}
// 不可修改
myClothes.price = 100
// error TS2540: Cannot assign to 'price' because it is a constant or a read-only property
TypeScript 可以通過(guò) ReadonlyArray<T>
設(shè)置數(shù)組為只讀,那么它的所有寫(xiě)方法都會(huì)失效。
let arr: ReadonlyArray<number> = [1,2,3,4,5];
arr[0] = 6; // Index signature in type 'readonly number[]' only permits reading
代碼解釋: 代碼中的泛型語(yǔ)法在之后會(huì)有專門的小節(jié)介紹。
4.2.1 readonly
vs const
最簡(jiǎn)單判斷該用 readonly
還是 const
的方法是看要把它做為變量使用還是做為一個(gè)屬性。做為 變量
使用的話用 const,若做為 屬性
則使用 readonly。
4.3 任意屬性
有時(shí)候我們希望接口允許有任意的屬性,語(yǔ)法是用 []
將屬性包裹起來(lái):
// 語(yǔ)法
interface Clothes {
color?: string;
size: string;
readonly price: number;
[propName: string]: any;
}
// 任意屬性 activity
let myClothes: Clothes = {
size: 'XL',
price: 98,
activity: 'coupon'
}
代碼解釋: 這里的接口 Clothes
可以有任意數(shù)量的屬性,并且只要它們不是 color
size
和 price
,那么就無(wú)所謂它們的類型是什么。
- 項(xiàng)目案例:使用 axios 庫(kù)發(fā)起 HTTP 傳輸?shù)臅r(shí)候,可以寫(xiě)入一個(gè)自定義的屬性,就是因?yàn)樵创a中定義了一個(gè)任意屬性:
this.$axios({
method: 'put',
url: '/cms/user',
data: {
nickname: this.nickname,
},
showBackend: true,
})
5. 函數(shù)類型
除了描述帶有屬性的普通對(duì)象外,接口也可以描述函數(shù)類型。
為了使接口表示函數(shù)類型,我們需要給接口定義一個(gè)調(diào)用簽名。 它就像是一個(gè)只有 參數(shù)列表
和 返回值類型
的函數(shù)定義。
interface SearchFunc {
(source: string, subString: string): boolean;
}
let mySearch: SearchFunc;
mySearch = function(source: string, subString: string): boolean {
return source.search(subString) > -1;
}
對(duì)于函數(shù)類型的類型檢查來(lái)說(shuō),函數(shù)的參數(shù)名不需要與接口里定義的名字相匹配。你可以改變函數(shù)的參數(shù)名,只要保證函數(shù)參數(shù)的位置不變。函數(shù)的參數(shù)會(huì)被逐個(gè)進(jìn)行檢查:
interface SearchFunc {
(source: string, subString: string): boolean;
}
let mySearch: SearchFunc;
// source => src, subString => sub
mySearch = function(src: string, sub: string): boolean {
return src.search(sub) > -1;
}
如果你不想指定類型,TypeScript 的類型系統(tǒng)會(huì)推斷出參數(shù)類型,因?yàn)楹瘮?shù)直接賦值給了 SearchFunc 類型變量。
interface SearchFunc {
(source: string, subString: string): boolean;
}
let mySearch: SearchFunc;
mySearch = function(src, sub) {
let result = src.search(sub);
return result > -1;
}
如果接口中的函數(shù)類型帶有函數(shù)名,下面兩種書(shū)寫(xiě)方式是等價(jià)的:
interface Calculate {
add(x: number, y: number): number
multiply: (x: number, y: number) => number
}
6. 可索引類型
可索引類型接口讀起來(lái)有些拗口,直接看例子:
// 正常的js代碼
let arr = [1, 2, 3, 4, 5]
let obj = {
brand: 'imooc',
type: 'education'
}
arr[0]
obj['brand']
再來(lái)看定義可索引類型接口:
interface ScenicInterface {
[index: number]: string
}
let arr: ScenicInterface = ['西湖', '華山', '故宮']
let favorite: string = arr[0]
示例中索引簽名是 number類型
,返回值是字符串類型。
另外還有一種索引簽名是 字符串類型
。我們可以同時(shí)使用兩種類型的索引,但是數(shù)字索引的返回值必須是字符串索引返回值類型的子類型。通過(guò)下面的例子理解這句話:
// 正確
interface Foo {
[index: string]: number;
x: number;
y: number;
}
// 錯(cuò)誤
interface Bar {
[index: string]: number;
x: number;
y: string; // Error: y 屬性必須為 number 類型
}
代碼解釋:
第 12 行,語(yǔ)法錯(cuò)誤是因?yàn)楫?dāng)使用 number 來(lái)索引時(shí),JavaScript 會(huì)將它轉(zhuǎn)換成 string 然后再去索引對(duì)象。也就是說(shuō)用 100(一個(gè)number)去索引等同于使用"100"(一個(gè)string)去索引,因此兩者需要保持一致。
7. 類類型
我們希望類的實(shí)現(xiàn)必須遵循接口定義,那么可以使用 implements
關(guān)鍵字來(lái)確保兼容性。
這種類型的接口在傳統(tǒng)面向?qū)ο笳Z(yǔ)言中最為常見(jiàn),比如 java 中接口就是這種類類型的接口。這種接口與抽象類比較相似,但是接口只能含有抽象方法和成員屬性,實(shí)現(xiàn)類中必須實(shí)現(xiàn)接口中所有的抽象方法和成員屬性。
interface AnimalInterface {
name: string;
}
class Dog implements AnimalInterface {
name: string;
constructor(name: string){
this.name = name
}
}
你也可以在接口中描述一個(gè)方法,在類里實(shí)現(xiàn)它:
interface AnimalInterface {
name: string
eat(m: number): string
}
class Dog implements AnimalInterface {
name: string;
constructor(name: string){
this.name = name
}
eat(m: number) {
return `${this.name}吃肉${m}分鐘`
}
}
接口描述了類的公共部分,而不是公共和私有兩部分。 它不會(huì)幫你檢查類是否具有某些私有成員。
8. 繼承接口
和類一樣,接口也可以通過(guò)關(guān)鍵字 extents
相互繼承。 這讓我們能夠從一個(gè)接口里復(fù)制成員到另一個(gè)接口里,可以更靈活地將接口分割到可重用的模塊里。
interface Shape {
color: string;
}
interface Square extends Shape {
sideLength: number;
}
let square = {} as Square;
// 繼承了 Shape 的屬性
square.color = "blue";
square.sideLength = 10;
一個(gè)接口可以繼承多個(gè)接口,創(chuàng)建出多個(gè)接口的合成接口。
interface Shape {
color: string;
}
interface PenStroke {
penWidth: number;
}
interface Square extends Shape, PenStroke {
sideLength: number;
}
let square = {} as Square;
square.color = "blue";
square.sideLength = 10;
square.penWidth = 5.0;
9. 混合類型
在前面已經(jīng)介紹,接口可以描述函數(shù)、對(duì)象的方法或者對(duì)象的屬性。
有時(shí)希望一個(gè)對(duì)象同時(shí)具有上面提到多種類型,比如一個(gè)對(duì)象可以當(dāng)做函數(shù)使用,同時(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;
代碼解釋:
第 1 行,聲明一個(gè)接口,如果只有 (start: number): string
一個(gè)成員,那么這個(gè)接口就是函數(shù)接口,同時(shí)還具有其他兩個(gè)成員,可以用來(lái)描述對(duì)象的屬性和方法,這樣就構(gòu)成了一個(gè)混合接口。
第 7 行,創(chuàng)建一個(gè) getCounter()
函數(shù),它的返回值是 Counter 類型的。
let counter = function (start: number) { } as Counter;
第 8 行,通過(guò)類型斷言,將函數(shù)對(duì)象轉(zhuǎn)換為 Counter
類型,轉(zhuǎn)換后的對(duì)象不但實(shí)現(xiàn)了函數(shù)接口的描述,使之成為一個(gè)函數(shù),還具有 interval 屬性和 reset() 方法。斷言成功的條件是,兩個(gè)數(shù)據(jù)類型只要有一方可以賦值給另一方,這里函數(shù)類型數(shù)據(jù)不能賦值給接口類型的變量,因?yàn)樗痪哂?interval 屬性和 reset() 方法。
類型斷言在之后的小節(jié)也會(huì)單節(jié)介紹。
10. 小結(jié)
本節(jié)介紹了接口的基本用法及其使用場(chǎng)景,接口在 TypeScript 中至關(guān)重要,TypeScript 編譯器依賴接口用于類型檢查。