TypeScript 接口 interface 使用詳解

我是法醫(yī),一只治療系前端碼猿??,與代碼對(duì)話,傾聽它們心底的呼聲,期待著大家的點(diǎn)贊??與關(guān)注?,當(dāng)然也歡迎加入前端獵手技術(shù)交流群??,文末掃碼我拉你進(jìn)群,一起交流技術(shù)以及代碼之外的一切???♀?
在 TypeScript中,接口是一個(gè)很重要的特性,它讓 TypeScript 具備了 JavaScript 所缺少的、描述較為復(fù)雜數(shù)據(jù)結(jié)構(gòu)的能力。下面就來(lái)看看什么是接口類型。
一、接口定義
接口是一系列抽象方法的聲明,是一些方法特征的集合,這些方法都應(yīng)該是抽象的,需要由具體的類去實(shí)現(xiàn),然后第三方就可以通過這組抽象方法調(diào)用,讓具體的類執(zhí)行具體的方法。
TypeScript 的核心原則之一是對(duì)值所具有的結(jié)構(gòu)進(jìn)行類型檢查,并且只要兩個(gè)對(duì)象的結(jié)構(gòu)一致,屬性和方法的類型一致,則它們的類型就是一致的。?在TypeScript里,接口的作用就是為這些類型命名和為代碼或第三方代碼定義契約。
TypeScript 接口定義形式如下:
interface?interface_name?{?}
來(lái)看例子,函數(shù)的參數(shù)是一個(gè)對(duì)象,它包含兩個(gè)字段:firstName 和 lastName,返回一個(gè)拼接后的完整名字:
const?getFullName?=?({?firstName,?lastName?})?=>?{
??return?`${firstName}?${lastName}`;
};
調(diào)用時(shí)傳入?yún)?shù):
getFullName({
??firstName:?"Hello",
??lastName:?"TypeScript"
});?
這樣調(diào)用是沒有問題的,但是如果傳入的參數(shù)不是想要的參數(shù)格式時(shí),就會(huì)出現(xiàn)一些錯(cuò)誤:
getFullName();?//?Uncaught?TypeError:?Cannot?destructure?property?`a`?of?'undefined'?or?'null'.
getFullName({?age:?18,?phone:?110?});?//?'undefined?undefined'
getFullName({?firstName:?"Hello"?});?//?'Hello?undefined'
這些都是我們不想要的,在開發(fā)時(shí)難免會(huì)傳入錯(cuò)誤的參數(shù),所以 TypeScript 能夠在編譯階段就檢測(cè)到這些錯(cuò)誤。下面來(lái)完善下這個(gè)函數(shù)的定義:
const?getFullName?=?({
??firstName,
??lastName,
}:?{
??firstName:?string;?//?指定屬性名為firstName和lastName的字段的屬性值必須為string類型
??lastName:?string;
})?=>?{
??return?`${firstName}?${lastName}`;
};
通過對(duì)象字面量的形式去限定傳入的這個(gè)對(duì)象的結(jié)構(gòu),現(xiàn)在再來(lái)看下之前的調(diào)用會(huì)出現(xiàn)什么提示:
getFullName();?//?應(yīng)有1個(gè)參數(shù),但獲得0個(gè)
getFullName({?age:?18,?phone:?110?});?//?類型“{ age: number; phone: number;?}”的參數(shù)不能賦給類型“{ firstName: string; lastName: string;?}”的參數(shù)。
getFullName({?firstName:?"Hello"?});?//?缺少必要屬性lastName
這些都是在編寫代碼時(shí) TypeScript 提示的錯(cuò)誤信息,這樣就避免了在使用函數(shù)的時(shí)候傳入不正確的參數(shù)。我們可以使用interface來(lái)定義接口:
interface?Info?{
??firstName:?string;
??lastName:?string;
}
const?getFullName?=?({?firstName,?lastName?}:?Info)?=>
??return?`${firstName}?${lastName}`;
};
注意:在定義接口時(shí),不要把它理解為是在定義一個(gè)對(duì)象,{}括號(hào)包裹的是一個(gè)代碼塊,里面是聲明語(yǔ)句,只不過聲明的不是變量的值而是類型。聲明也不用等號(hào)賦值,而是冒號(hào)指定類型。每條聲明之前用換行分隔即可,也可以使用分號(hào)或者逗號(hào)。
二、接口屬性
1. 可選屬性
在定義一些結(jié)構(gòu)時(shí),一些結(jié)構(gòu)的某些字段的要求是可選的,有這個(gè)字段就做處理,沒有就忽略,所以針對(duì)這種情況,TypeScript提供了可選屬性。
定義一個(gè)函數(shù):
const?getVegetables?=?({?color,?type?})?=>?{
??return?`A?${color???color?+?"?"?:?""}${type}`;
};
這個(gè)函數(shù)中根據(jù)傳入對(duì)象中的 color 和 type 來(lái)進(jìn)行描述返回一句話,color 是可選的,所以可以給接口設(shè)置可選屬性,在屬性名后面加個(gè)?即可:
interface?Vegetables?{
??color?:?string;
??type:?string;
}
const?getVegetables?=?({?color,?type?}:?Vegetables)?=>?{
??return?`A?${color???color?+?"?"?:?""}${type}`;
};
這里可能會(huì)報(bào)一個(gè)警告:接口應(yīng)該以大寫的i開頭,可以在 tslint.json 的 rules 里添加"interface-name": [true, “never-prefix”]來(lái)關(guān)閉這條規(guī)則。
當(dāng)屬性被標(biāo)注為可選后,它的類型就變成了顯式指定的類型與 undefined 類型組成的聯(lián)合類型,比如 getVegetables 方法的參數(shù)中的 color 屬性類型就變成了這樣:
string?|?undefined;
那下面的接口與上述接口是一樣的嗎?
interface?Vegetables2?{
??color?:?string?|?undefined;
??type:?string;
}
答案肯定是否定的,因?yàn)榭蛇x意味著可以不設(shè)置屬性鍵名,類型是 undefined 意味著屬性鍵名不可選。
2. 只讀屬性
接口可以設(shè)置只讀屬性,如下:
interface?Role?{
??readonly?0:?string;
??readonly?1:?string;
}
這里定義了一個(gè)角色,有 0 和 1 兩種角色 id。下面定義一個(gè)角色數(shù)據(jù),并修改一下它的值:
const?role:?Role?=?{
??0:?"super_admin",
??1:?"admin"
};
role[1]?=?"super_admin";?//?Cannot?assign?to?'0'?because?it?is?a?read-only?property
這里TypeScript 提示不能分配給索引0,因?yàn)樗侵蛔x屬性。
在ES6中,使用const定義的常量定義之后不能再修改,這和只讀意思接近。那readonly和const在使用時(shí)該如何選擇呢?那主要看這個(gè)值的用途,如果是定義一個(gè)常量,那用const,如果這個(gè)值是作為對(duì)象的屬性,就用readonly:
const?NAME:?string?=?"TypeScript";
NAME?=?"Haha";?//?Uncaught?TypeError:?Assignment?to?constant?variable
const?obj?=?{
??name:?"TypeScript"
};
obj.name?=?"Haha";
interface?Info?{
??readonly?name:?string;
}
const?info:?Info?=?{
??name:?"TypeScript"
};
info["name"]?=?"Haha";?//?Cannot?assign?to?'name'?because?it?is?a?read-only?property
上面使用const定義的常量NAME定義之后再修改會(huì)報(bào)錯(cuò),但是如果使用const定義一個(gè)對(duì)象,然后修改對(duì)象里屬性的值是不會(huì)報(bào)錯(cuò)的。所以如果要保證對(duì)象的屬性值不可修改,需要使用readonly。
需要注意,readonly只是靜態(tài)類型檢測(cè)層面的只讀,實(shí)際上并不能阻止對(duì)對(duì)象的修改。因?yàn)樵谵D(zhuǎn)譯為 JavaScript 之后,readonly 修飾符會(huì)被抹除。因此,任何時(shí)候與其直接修改一個(gè)對(duì)象,不如返回一個(gè)新的對(duì)象,這是一種比較安全的方式。
3. 多余屬性檢查
先來(lái)看下面的例子:
interface?Vegetables?{
??color?:?string;
??type:?string;
}
const?getVegetables?=?({?color,?type?}:?Vegetables)?=>?{
??return?`A?${color???color?+?"?"?:?""}${type}`;
};
getVegetables({
??type:?"tomato",
??size:?"big"?????//?'size'不在類型'Vegetables'中
});
這里沒有傳入 color 屬性,因?yàn)樗且粋€(gè)可選屬性,所以沒有報(bào)錯(cuò)。但是多傳入了一個(gè) size 屬性,這時(shí)就會(huì)報(bào)錯(cuò),TypeScript就會(huì)提示接口上不存在這個(gè)多余的屬性,所以只要是接口上沒有定義這個(gè)屬性,在調(diào)用時(shí)出現(xiàn)了,就會(huì)報(bào)錯(cuò)。
注意:?這里可能會(huì)報(bào)一個(gè)警告:屬性名沒有按開頭字母順序排列屬性列表,可以在 tslint.json 的 rules 里添加"object-literal-sort-keys": [false]來(lái)關(guān)閉這條規(guī)則。
有時(shí) 不希望TypeScript這么嚴(yán)格的對(duì)數(shù)據(jù)進(jìn)行檢查,比如上面的函數(shù),只需要保證傳入getVegetables的對(duì)象有type屬性就可以了,至于實(shí)際使用的時(shí)候傳入對(duì)象有沒有多余的屬性,多余屬性的屬性值是什么類型,我們就不管了,那就需要繞開多余屬性檢查,有如下方式:
(1) 使用類型斷言
類型斷言就是告訴 TypeScript,已經(jīng)自行進(jìn)行了檢查,確保這個(gè)類型沒有問題,希望 TypeScript 對(duì)此不進(jìn)行檢查,這是最簡(jiǎn)單的實(shí)現(xiàn)方式,類型斷言使用 as 關(guān)鍵字來(lái)定義(這里不細(xì)說,后面進(jìn)階篇會(huì)專門介紹類型斷言):
interface?Vegetables?{
??color?:?string;
??type:?string;
}
const?getVegetables?=?({?color,?type?}:?Vegetables)?=>?{
??return?`A?${color???color?+?"?"?:?""}${type}`;
};
getVegetables({
??type:?"tomato",
??size:?12,
??price:?1.2
}?as?Vegetables);
(2) 添加索引簽名
更好的方式是添加字符串索引簽名:
interface?Vegetables?{
??color:?string;
??type:?string;
??[prop:?string]:?any;
}
const?getVegetables?=?({?color,?type?}:?Vegetables)?=>?{
??return?`A?${color???color?+?"?"?:?""}${type}`;
};
getVegetables({
??color:?"red",
??type:?"tomato",
??size:?12,
??price:?1.2
});
三、接口使用
1. 定義函數(shù)類型
在前面函數(shù)類型篇我們說了,可以使用接口來(lái)定義函數(shù)類型:
interface?AddFunc?{
??(num1:?number,?num2:?number):?number;
}
這里定義了一個(gè)AddFunc結(jié)構(gòu),這個(gè)結(jié)構(gòu)要求實(shí)現(xiàn)這個(gè)結(jié)構(gòu)的值,必須包含一個(gè)和結(jié)構(gòu)里定義的函數(shù)一樣參數(shù)、一樣返回值的方法,或者這個(gè)值就是符合這個(gè)函數(shù)要求的函數(shù)。把花括號(hào)里包著的內(nèi)容稱為調(diào)用簽名,它由帶有參數(shù)類型的參數(shù)列表和返回值類型組成:
const?add:?AddFunc?=?(n1,?n2)?=>?n1?+?n2;
const?join:?AddFunc?=?(n1,?n2)?=>?`${n1}?${n2}`;?//?不能將類型'string'分配給類型'number'
add("a",?2);?//?類型'string'的參數(shù)不能賦給類型'number'的參數(shù)
上面定義的add函數(shù)接收兩個(gè)數(shù)值類型的參數(shù),返回的結(jié)果也是數(shù)值類型,所以沒有問題。而join函數(shù)參數(shù)類型沒錯(cuò),但是返回的是字符串,所以會(huì)報(bào)錯(cuò)。而當(dāng)調(diào)用add函數(shù)時(shí),傳入的參數(shù)如果和接口定義的類型不一致,也會(huì)報(bào)錯(cuò)。在實(shí)際定義函數(shù)的時(shí)候,名字是無(wú)需和接口中參數(shù)名相同的,只需要位置對(duì)應(yīng)即可。
實(shí)際上,很少使用接口類型來(lái)定義函數(shù)類型,更多使用內(nèi)聯(lián)類型或類型別名配合箭頭函數(shù)語(yǔ)法來(lái)定義函數(shù)類型:
type?AddFunc?=?(num1:?number,?num2:?number)?=>?number;
這里給箭頭函數(shù)類型指定了一個(gè)別名 AddFunc,在其他地方就可以直接復(fù)用 AddFunc,而不用重新聲明新的箭頭函數(shù)類型定義。
2. 定義索引類型
在實(shí)際工作中,使用接口類型較多的地方是對(duì)象,比如 React 組件的 Props & State等,這些對(duì)象有一個(gè)共性,即所有的屬性名、方法名都是確定的。
實(shí)際上,經(jīng)常會(huì)把對(duì)象當(dāng) Map 映射使用,比如下邊代碼中定義了索引是任意數(shù)字的對(duì)象 role1 和索引是任意字符串的對(duì)象 role2。
const?role1?=?{
??0:?"super_admin",
??1:?"admin"
};
const?role2?=?{
??s:?"super_admin",??
??a:?"admin"
};
這時(shí)需要使用索引簽名來(lái)定義上邊提到的對(duì)象映射結(jié)構(gòu),并通過 “[索引名: 類型]”的格式約束索引的類型。索引名稱的類型分為 string 和 number 兩種,通過如下定義的 RoleDic 和 RoleDic1 兩個(gè)接口,可以用來(lái)描述索引是任意數(shù)字或任意字符串的對(duì)象:
interface?RoleDic?{
??[id:?number]:?string;
}
interface?RoleDic1?{
??[id:?string]:?string;
}
const?role1:?RoleDic?=?{
??0:?"super_admin",
??1:?"admin"
};
const?role2:?RoleDic?=?{
??s:?"super_admin",??// error 不能將類型"{ s: string; a: string;?}"分配給類型"RoleDic"。
??a:?"admin"
};
const?role3:?RoleDic?=?["super_admin",?"admin"];
需要注意,當(dāng)使用數(shù)字作為對(duì)象索引時(shí),它的類型既可以與數(shù)字兼容,也可以與字符串兼容,這與 JavaScript 的行為一致。因此,使用 0 或 '0' 索引對(duì)象時(shí),這兩者等價(jià)。
上面的 role3 定義了一個(gè)數(shù)組,索引為數(shù)值類型,值為字符串類型。我們還可以給索引設(shè)置readonly,從而防止索引返回值被修改:
interface?RoleDic?{
??readonly?[id:?number]:?string;
}
const?role:?RoleDic?=?{
??0:?"super_admin"
};
role[0]?=?"admin";?//?error?類型"RoleDic"中的索引簽名僅允許讀取
注意,可以設(shè)置索引類型為 number。但是這樣如果將屬性名設(shè)置為字符串類型,則會(huì)報(bào)錯(cuò);但是如果設(shè)置索引類型為字符串類型,那么即便屬性名設(shè)置的是數(shù)值類型,也沒問題。因?yàn)?JS 在訪問屬性值時(shí),如果屬性名是數(shù)值類型,會(huì)先將數(shù)值類型轉(zhuǎn)為字符串,然后再去訪問:
const?obj?=?{
??123:?"a",?
??"123":?"b"?//?報(bào)錯(cuò):標(biāo)識(shí)符“"123"”重復(fù)。
};
console.log(obj);?//?{?'123':?'b'?}
如果數(shù)值類型的屬性名不會(huì)轉(zhuǎn)為字符串類型,那么這里數(shù)值123和字符串123是不同的兩個(gè)值,則最后對(duì)象obj應(yīng)該同時(shí)有這兩個(gè)屬性;但是實(shí)際打印出來(lái)的obj只有一個(gè)屬性,屬性名為字符串"123",值為"b",說明數(shù)值類型屬性名123被覆蓋掉了,就是因?yàn)樗晦D(zhuǎn)為了字符串類型屬性名"123";又因?yàn)橐粋€(gè)對(duì)象中多個(gè)相同屬性名的屬性,定義在后面的會(huì)覆蓋前面的,所以結(jié)果就是obj只保留了后面定義的屬性值。
四、高級(jí)用法
1. 繼承接口
在 TypeScript 中,接口是可以繼承,這和ES6中的類一樣,這提高了接口的可復(fù)用性。來(lái)看一個(gè)場(chǎng)景:定義一個(gè)Vegetables接口,它會(huì)對(duì)color屬性進(jìn)行限制。再定義兩個(gè)接口Tomato和Carrot,這兩個(gè)類都需要對(duì)color進(jìn)行限制,而各自又有各自獨(dú)有的屬性限制,可以這樣定義:
interface?Vegetables?{
??color:?string;
}
interface?Tomato?{
??color:?string;
??radius:?number;
}
interface?Carrot?{
??color:?string;
??length:?number;
}
三個(gè)接口中都有對(duì)color的定義,但是這樣寫很繁瑣,可以用繼承來(lái)改寫:
interface?Vegetables?{
??color:?string;
}
interface?Tomato?extends?Vegetables?{
??radius:?number;
}
interface?Carrot?extends?Vegetables?{
??length:?number;
}
const?tomato:?Tomato?=?{
??radius:?1.2?//?error??Property?'color'?is?missing?in?type?'{?radius:?number;?}'
};
const?carrot:?Carrot?=?{
??color:?"orange",
??length:?20
};
上面定義的?tomato?變量因?yàn)槿鄙倭藦?code style="margin-right: 2px;margin-left: 2px;padding: 2px 4px;outline: 0px;max-width: 100%;overflow-wrap: break-word;font-size: 14px;border-radius: 4px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(239, 112, 96);box-sizing: border-box !important;">Vegetables接口繼承來(lái)的?color?屬性,所以報(bào)錯(cuò)了。
一個(gè)接口可以被多個(gè)接口繼承,同樣,一個(gè)接口也可以繼承多個(gè)接口,多個(gè)接口用逗號(hào)隔開。比如再定義一個(gè)Food接口,Tomato?也可以繼承?Food:
interface?Vegetables?{
??color:?string;
}
interface?Food?{
??type:?string;
}
interface?Tomato?extends?Food,?Vegetables?{
??radius:?number;
}
const?tomato:?Tomato?=?{
??type:?"vegetables",
??color:?"red",
??radius:?1
};??
如果想要覆蓋掉繼承的屬性,那就只能使用兼容的類型進(jìn)行覆蓋:
interface?Tomato?extends?Vegetables?{
??color:?number;
}
這里我們將color屬性進(jìn)行了覆蓋,并將其類型設(shè)置為了number類型,這時(shí)就會(huì)報(bào)錯(cuò),因?yàn)門omato 和 Vegetables 中的name屬性是不兼容的。
2. 混合類型接口
在 JavaScript 中,函數(shù)是對(duì)象類型。對(duì)象可以有屬性,所以有時(shí)一個(gè)對(duì)象既是一個(gè)函數(shù),也包含一些屬性。比如要實(shí)現(xiàn)一個(gè)計(jì)數(shù)器函數(shù),比較直接的做法是定義一個(gè)函數(shù)和一個(gè)全局變量:
let?count?=?0;
const?counter?=?()?=>?count++;
但是這種方法需要在函數(shù)外面定義一個(gè)變量,更優(yōu)一點(diǎn)的方法是使用閉包:
const?counter?=?(()?=>?{
??let?count?=?0;
??return?()?=>?{
????return?count++;
??};
})();
console.log(counter());?//?1
console.log(counter());?//?2
TypeScript 支持直接給函數(shù)添加屬性,這在 JavaScript 中是支持的:
let?counter?=?()?=>?{
??return?counter.count++;
};
counter.count?=?0;
console.log(counter());?//?1
console.log(counter());?//?2
這里把一個(gè)函數(shù)賦值給countUp,又給它綁定了一個(gè)屬性count,計(jì)數(shù)保存在這個(gè)?count?屬性中。
可以使用混合類型接口來(lái)指定上面例子中?counter?的類型:
interface?Counter?{
??():?void;?
??count:?number;?
}
const?getCounter?=?():?Counter?=>?{?
??const?c?=?()?=>?{?
????c.count++;
??};
??c.count?=?0;?
??return?c;?
};
const?counter:?Counter?=?getCounter();?
counter();
console.log(counter.count);?//?1
counter();
console.log(counter.count);?//?2
這里定義了一個(gè)Counter接口,這個(gè)結(jié)構(gòu)必須包含一個(gè)函數(shù),函數(shù)的要求是無(wú)參數(shù),返回值為void,即無(wú)返回值。而且這個(gè)結(jié)構(gòu)還必須包含一個(gè)名為count、值的類型為number類型的屬性。最后,通過getCounter函數(shù)得到這個(gè)計(jì)數(shù)器。這里?getCounter?的類型為Counter,它是一個(gè)函數(shù),無(wú)返回值,即返回值類型為void,它還包含一個(gè)屬性count,屬性返回值類型為number。
五、類型別名
類型別名并不屬于接口類型的內(nèi)容,但是它和接口功能類似,所以這里放在一起來(lái)說。
1. 基本使用
接口類型的作用就是將內(nèi)聯(lián)類型抽離出來(lái),從而實(shí)現(xiàn)類型復(fù)用。其實(shí),還可以使用類型別名接收抽離出來(lái)的內(nèi)聯(lián)類型實(shí)現(xiàn)復(fù)用。可以通過如下所示“type 別名名字 = 類型定義”的格式來(lái)定義類型別名,比如上面定義的計(jì)數(shù)器方法的類型:
type?Counter?=?{
??():?void;?
??count:?number;?
}
這樣寫看起來(lái)就像是在JavaScript中定義變量,只不過是把var、let、const換成了type。實(shí)際上,類型別名可以在接口類型無(wú)法覆蓋的場(chǎng)景中使用,比如聯(lián)合類型、交叉類型等:
//?聯(lián)合類型
type?Name?=?number?|?string;
//?交叉類型
type?Vegetables?=?{color:?string,?radius:?number}?&?{color:?string,?length:?number}
這里定義了一個(gè) Vegetables 類型別名,表示兩個(gè)匿名接口類型交叉出的類型。
需要注意:類型別名只是給類型取了一個(gè)別名,并不是創(chuàng)建了一個(gè)新的類型。
2. 與接口區(qū)別
通過上面的介紹,可以發(fā)現(xiàn)多數(shù)情況下是可以使用類型別名來(lái)替代的,那這是否說明這兩者是等價(jià)的呢?答案肯定是否定的,不然也不會(huì)出這兩個(gè)概念。在某些特定的場(chǎng)景下,這兩者還是存在很大區(qū)別。比如,重復(fù)定義的接口類型,它的屬性會(huì)疊加,這個(gè)特性使得我們可以很方便地對(duì)全局變量、第三方庫(kù)的類型做擴(kuò)展:
interface?Vegetables?{
??color:?string;
}
interface?Vegetables?{
??radius:?number;
}
interface?Vegetables?{
??length:?number;
}
let?vegetables:?Vegetables?=?{
?color:??"red",
??radius:?2,
??length:?10
}
這里我們定義了三次 Vegetables 接口,此時(shí)可以賦值給變一個(gè)包含color、radius、length的對(duì)象,并且不會(huì)報(bào)錯(cuò)。
如果重復(fù)定義類型別名:
type?Vegetables?=?{
??color:?string;
}
type?Vegetables?=?{
??radius:?number;
}
type?Vegetables?=?{
??length:?number;
}
let?vegetables:?Vegetables?=?{
?color:??"red",
??radius:?2,
??length:?10
}
上述代碼就會(huì)報(bào)錯(cuò):'Vegetables' is already defined。所以,接口類型是可重復(fù)定義且屬性會(huì)疊加的,而類型別名是不可重復(fù)定義的。
很感謝小伙伴看到最后??,如果您覺得這篇文章有幫助到您的的話不妨關(guān)注?+點(diǎn)贊??+收藏??+評(píng)論??,您的支持就是我更新的最大動(dòng)力。
歡迎加入前端獵手技術(shù)交流群??,文末掃碼加我微信,我拉你進(jìn)群,一起交流技術(shù)以及代碼之外的一切???♀?
