前言
眾所周知,在傳統(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í)候,給x和y分配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類中必須有類型為string的currentTime變量。
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)類型語言和弱類型語言的特性的理解。