- TypeScript接口的基本使用
- TypeScript函數類型接口
- TypeScript可索引類型接口
- TypeScript類類型接口
- TypeScript接口與繼承
一、TypeScript接口的基本使用
1.1定義TypeScript接口的指令(interface)
接口讓我們想到的第一個短語就是(API)。比如日常我們所說的調用某某程序的API一般都是跨應用的接口,如別的網站的或者APP的接口;還有我們每天都是用的編程語言的指令和內置的方法及屬性,這些可以叫做編程語言的接口;還有令我們既愛又恨的各種平台、框架、庫的龐大接口集,特別是這些東西每一次升級都會產生新的接口或者修改以前的接口,這些接口需要我們付出大量的學習時間。
而學習接口第一要務就是學習接口的語法,這些語法往往定義了各種讀寫規則,這里拋開接口背后實現的意義不談,因為學習使用TypeScript接口主要就是學會如何自定義各種接口的規則,至於你將來要在程序中定義什么樣的接口、根據什么定義接口規則都是要根據程序本身的性質決定的。
比如按照我們JS的形式給一個函數定義參數接口:
1 function printLabel(labelledObj: { label: string }) { 2 console.log(labelledObj.label); 3 } 4 let myObj = { size: 10, label: "Size 10 Object" }; 5 printLabel(myObj);
這里會有類型檢查幫助我們檢查傳入(printLabel)函數的參數是否有指定的屬性(label)。然后,來看看TypeScript接口如何定義上面這個參數接口:
interface LabelledValue { label: string; } function printLabel(labelledObj: LabelledValue) {//這里指定參數要符合LabelledValue接口規則,參數不許包含一個label屬性,且類型為string console.log(labelledObj.label); } let myObj = {size: 10, label: "Size 10 Object"}; printLabel(myObj);
感覺這兩種實現方式沒有什么區別,但是對於嚴格TypeScript的接口來說就沒有這門寬松了,TypeScript包含非常豐富的接口規則,定義TypeScript接口最基本的規則就有:可選屬性、只讀屬性、額外屬性檢查。
1.2可選屬性(option bags模式):在可選屬性名稱定義的后面加上(?)問號。注意使用可選屬性后,就不能出現額外的屬性了,除非使用后面的額外屬性檢查規則實現。
1 interface SquareConfig { 2 color?: string;//定義為接口的額外屬性 3 width?: number;//定義為接口的額外屬性 4 } 5 function createSquare(config: SquareConfig): {color: string; area: number} { 6 let newSquare = {color: "white", area: 100}; 7 if (config.color) { 8 newSquare.color = config.color; 9 } 10 if (config.width) { 11 newSquare.area = config.width * config.width; 12 } 13 return newSquare; 14 } 15 let mySquare = createSquare({color: "black"} );//可選擇傳入部分可選屬性 16 let mySquare1 = createSquare({color: "black",width:120} ); 17 // let mySquare2 = createSquare({color: "black",widtg:120} );//不能出現屬性名不一致,這同樣被識別為額外屬性,出現2345錯誤 18 // let mySquare3 = createSquare({color: "black",width:120,size:120} );//這里出現了一個額外屬性size,會出現2345錯誤
1.3只讀屬性:實現只讀屬性接口時不能不寫只讀屬性,並且也不能出現額外屬性,除非使用額外屬性檢查。
readonly(指定屬性只讀):在接口類型中指定的屬性前面添加readonly修飾符來指定該屬性為指定。
ReadonlyArray<T>(定義只讀數組):定義只讀數組類型結構的接口直接使用ReadonlyArray<T>類型來定義就可以了,不需要使用interface關鍵字。在官方文檔中提到了ReadonlyArray<T>的底層實現就是將數組的可變方法去掉了。
1 //readonly 2 interface Point { 3 readonly x: number; 4 readonly y: number; 5 } 6 function fun(p:Point):number{ 7 return p.x + p.y; 8 } 9 fun({x:1,y:2}) 10 // fun({x:1})//只讀屬性必須全部寫入參數 11 // fun({x:1,y:2,z:3})//不能添加額外的屬性 12 fun({x:1,y:2,z:3} as Point)//這里使用了額外屬性檢查就不會報錯 13 14 //ReadonlyArray<T>定義只讀數組 15 let a:number[] = [1,2,3]; 16 let ro:ReadonlyArray<number> = a; 17 let ro1:ReadonlyArray<number> = [1,2,3,4]; 18 console.log(ro == ro1); 19 ro1 = a; 20 // a = ro; //number[] 類型不等於ReadonlyArray<T> ,這是變量類型那不內容的延伸了,TypeScript對變量的類型有嚴格的要求 21 // a = ro1;//同上 22 a = ro as number[];//可以使用斷言重寫ro來實現類型轉換 23 // ro[1] = 100;//只讀數組不能重寫元素值
1.4額外屬性檢查:在前面的可選屬性和只讀屬性都提到了額外屬性檢查,額外屬性檢查顧名思義就是接口中出現了額外的屬性,而當在接口中定義可選和只讀規則以后就不能出現額外屬性,但是有時候在一些實現中可能出現額外屬性就可以使用額外屬性檢查來實現。
額外屬性檢查就是在實現接口時使用“as”來告訴編譯環境,這里允許出現額外屬性,只要除額外屬性以外的其他屬性符合接口的規則就允許實現。
1 interface SquareConfig { 2 color?: string; 3 width?: number; 4 } 5 function createSquare(config: SquareConfig): { color: string; area: number } { 6 // ... 7 } 8 let mySquare = createSquare({ colour: "red", width: 100 });//這里不小心把color寫成了colour,colour被識別為額外屬性了 9 let mySquare1 = createSquare({ colour: "red", width: 100 } as SquareConfig);//使用額外屬性檢查就可以解決這類報錯
關於額外屬性檢查,看是可以用來解決一些問題,實際上也確實可以解決一些問題,但是就上面的例子來說,這並不是一個好的結局方案,因為我們期望的時傳入color的屬性值,但是由於編碼失誤寫錯了單詞,額外屬性幫我們靜默了這個錯誤,也同時讓我們的程序偏離了我們期望的方向。關於這樣的問題后面有一種索引簽名的方式來解決,也就是后面的可索引的類型。
二、TypeScript函數類型接口
在TypeScript函數部分介紹了使用type定義Ts函數類型(詳細可以了解官方文檔或者這篇博客:TypeScript入門三:TypeScript函數類型),TypeScript接口也可以定義函數的接口類型,定義模式與type定義函數類型非常類似。
1 type myAdd = (baseValue: number, increment: number) => number; 2 interface inAdd { 3 (baseValue: number, increment: number):number; 4 } 5 //根據函數類型和接口聲明函數變量 6 let myAdd1:myAdd = function(x:number,y:number) : number{ 7 return x + y; 8 } 9 let myAdd2:inAdd = function(x:number,y:number) : number{ 10 return x - y; 11 }
函數實現函數接口和函數類的實例化都一樣,其都屬於函數內部語法,比如可以設置參數默認值;參數名稱與函數類或者接口定義的可以不一致,只要參數對應的位置實現的參數類型一致就可以了。
三、TypeScript可索引類型接口
在前面的額外屬性檢查中就提到了可索引類型接口,並有說過可以通過可索引類型接口解決由於書寫錯誤,並由額外屬性導致的錯誤靜默問題。先來了解可索引接口規則是什么?再看看如何解決前面的問題。
3.1數字索引的可索引類型接口:數字可索引類型其實本質上就是定義個數組類型,只要賦值符合該接口的定義就可以直接賦值。
1 interface StringArray { 2 [index: number]: string; 3 } 4 5 let myArray: StringArray; 6 myArray = ["Bob", "Fred"]; 7 let a = ["1","2","3"]; 8 myArray = a;//這里並不會失敗,數字可索引類型其實本質上就是定義個數組類型,只要賦值符合該接口的定義就可以直接賦值 9 let myStr: string = myArray[0];
3.2字符串可索引類型接口:字符串可索引類型其本質就是定義對象類型,只要賦值符合該接口定義就可以直接賦值。
1 interface StringObject { 2 [key:string]:string 3 } 4 let myObj : StringObject = { 5 'name':'他鄉踏雪', 6 'professional':'Web front end' 7 }
3.3字符串和數字兩種索引簽名組合使用:
這種復雜的接口應用在官方文檔中只給出了一個引導性的示例,官方使用了文字描述使用方法,而示例只給出了錯誤使用提示,並沒有給完整的正確示例,估計這讓很多初學者比較頭疼,我這兩天到各個教程中尋找完整的正確示例,但都沒有找到,最后通過n次測試終於實現了兩種索引組合使用的正確示例。直接上代碼,在代碼的注釋中解析這種接口實現的規則:
1 class Animal { 2 //定義類作為接口索引指向的值時: 3 // 類內部的成員必須初始化賦值或者基於構造函數實例化賦值 4 // 這里我采用了構造函數實例化賦值 5 name: string; 6 constructor(nameStr:string){ 7 this.name = nameStr; 8 } 9 } 10 class Dog extends Animal { 11 breed: string; 12 constructor(breedStr:string,nameStr:string){ 13 super(nameStr); 14 this.breed = breedStr; 15 } 16 } 17 // 數字和字符串索引簽名組合使用時: 18 // 數字索引賦值類型必須是字符串索引賦值類型的子類型 19 // 這個示例中數字索引賦值的Dog類型就是字符串索引賦值的Animal的子類型 20 21 //接口中定義索引時索引簽名的類型是關鍵, 22 // 索引定義的標識名稱(示例中的‘x’)並不重要, 23 // 可以使用任何合法字符,我沒有測試過特殊字符, 24 // 但實際上它是無意意義的,比如示例中兩個x可以替換成x和y照樣不影響接口實現 25 interface animals { 26 [x: number]: Dog; 27 [x: string]: Animal; 28 } 29 //基於數字與字符串索引組合的接口實現, 30 // 使用{}定義成對象類型 31 // 不能使用[]只實現數字索引的值 32 // 但可以使用{}只實現字符串索引的值 33 let myAnimals : animals = { 34 1:new Dog('Dog1','animal1'), 35 '2':new Dog('Dog2','animal2'), 36 'animal1':new Animal('animal1'), 37 'animal2':new Animal('animal2') 38 } 39 // let myAnimals1 : animals = [new Dog('Dog1','animal1')];//報錯 40 let myAnimals2 : animals = { //這里不報錯 41 'animal1':new Animal('animal1'), 42 'animal2':new Animal('animal2') 43 } 44 45 console.log(myAnimals);
在可索引類型類型接口中照樣可以使用只讀屬性規則:
1 interface ReadonlyStringArray { 2 readonly [index: number]: string; 3 } 4 let myArray: ReadonlyStringArray = ["Alice", "Bob"]; 5 myArray[2] = "Mallory"; // error!
前面說要基於可索引類型接口展示一個解決額外屬性產生的潛在問題,寫到這里我感覺好像沒有多大必要了,如果你真的搞懂了可索引類型接口規則,應該很輕松想到它可以如何實現,還有考慮到篇幅問題,就不在這里展示了,如果有需要可以在評論區留言。
四、TypeScript類類型接口
TypeScript類類型接口與抽象類非常類型,都是用來定義類的必須結構,但是類類型接口與抽象類還是有些區別,抽象類中可以實現具體的細節,但是在類類型接口中不能實現具體的細節。比如在抽象類中可以直接實現方法的具體內容,但是在類類型接口只能聲明一個方法的類型(包括名稱、參數類型、返回值類型),但不能在類類型接口中直接實現函數體的具體內容。
1 interface ClockInterface { 2 currentTime: Date;//指定強制實現的公共成員,在接口實現中必須初始化或者在構造其中賦值 3 setTime(d: Date):void;//指定強制實現的函數,在接口實現中必須實現,之一這里定義時需要有指定的返回值類型,void表示沒有任何類型,也就是不返任何類型的回值 4 5 } 6 7 class Clock implements ClockInterface { 8 currentTime: Date; 9 //↑上面這里第一種是想方法:實現接口強制約定要實現的公共成員currentTime,在構造器中賦值實現 10 //↓ 下面這里第二種實現方法: 實現接口強制約定要實現的公共成員currentTime,初始化值的方式實現 11 // currentTime: Date = new Date(); 12 13 setTime(d: Date) {//函數默認不返回任何類型的值,默認值為undefined,但它不是undefined類型 14 this.currentTime = d; 15 } 16 constructor(h: number, m: number) { 17 this.currentTime = new Date(); 18 } 19 }
關於類類型接口與抽象類的區別還有:
增加其他內容與否:派生類實現抽象類時不能隨意增加其他內容,一切根據抽象類定義的結構實現,但是在類類型接口的具體實現中可以增加類類型接口中沒有定義的內容。
公共成員與私有成員:抽象類中可以實現公共成員,也可以實現私有成員。但是,類類型接口中非靜態成員都是公共公共成員,而且並不需要public修飾,自動被默認為公共成員。
靜態成員在抽象類和類類型接口中都可以直接使用static修飾符來定義。
上面都是關於修飾符的相關內容,還有一個問題就是類類接口中不能直接定義構造函數的相關構造結構,在官方文檔中描述為類的靜態部分,也就常說的constructor函數。而當對一個類類型接口做具體實現時,只會對類類型的實體部分進行檢查,而不會對靜態部分檢查。
而且定義類類型接口時並不能直接使用constructor在類類型接口中定義相關結構,IDE會直接提示錯誤信息,官方給出的解決方案時使用構造器簽名的方式來實現。具體的實現方案就是定義一個構造器簽名的類類型接口、定義一個實體部分的類類型接口、然后在根據這兩個類類接口的結構的具體實現寫在一個類中,這個類的implements指向實體部分的類類型接口、最后定一個實例化工具方法,將方法的第一個參數類型設定為構造器簽名的類類型接口類型,實現構造需要的參數跟着寫后面,然后指定這個方法的返回類型為實體部分的類類型接口類型,方法內直接返回第一個參數的實例化,實例化時傳入對應需要的實例化參數。
具體見下面這個官方文檔中的示例,對照下面的注釋應該很清晰了:
1 interface ClockConstructor {//定義構造器簽名的類類型接口:作為類類型接口的構造函數的結構 2 new (hour: number, minute: number): ClockInterface;//構造函數的方法值設定為類類型主體類型 3 } 4 interface ClockInterface {//定義類類型接口的實體 5 tick():void; 6 } 7 //定義一個類類型接口實現的實例化工具方法,設置參數有:將類類型實現的類的類型設置為構造簽名的類類型接口類型,以及構造器需要的參數 8 // 返回值類型設置為類類型接口實體類型 9 function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface { 10 return new ctor(hour, minute);//內部直接通過類類型的具體實現類實例化並返回即可 11 } 12 //類類型接口的具體實現,implements指向類類型接口的實體類型 13 class DigitalClock implements ClockInterface { 14 constructor(h: number, m: number) { }//內部構造函數根據定義的構造器簽名的類類型接口結構實現 15 tick() { 16 console.log("beep beep"); 17 } 18 } 19 class AnalogClock implements ClockInterface { 20 constructor(h: number, m: number) { } 21 tick() { 22 console.log("tick tock"); 23 } 24 } 25 //將類類型接口的具體實現類作為參數以及構造函數需要的參數一並傳入,實例化類類型接口具體實現的類 26 let digital = createClock(DigitalClock, 12, 17); 27 let analog = createClock(AnalogClock, 7, 32);
五、TypeScript接口與繼承
接口繼承與類繼承沒什么差異,通過繼承可以將被繼承的接口內容復制過來,通過繼承可以更靈活的將接口分割重用,這里我就直接復制官方文檔的一個示例代碼展示了,因為從接口語法角度來說非常簡單,但是如果要使用上面類類型接口那樣的復雜接口來繼承另當別論。
1 interface Shape { 2 color: string; 3 } 4 5 interface PenStroke { 6 penWidth: number; 7 } 8 9 interface Square extends Shape, PenStroke { 10 sideLength: number; 11 } 12 13 let square = <Square>{}; 14 square.color = "blue"; 15 square.sideLength = 10; 16 square.penWidth = 5.0;
接口實現混合類型(函數類型與函數對象的靜態方法接口):
在前面已經介紹了關於函數類型接口的實現,但是僅僅只能實現一個單獨的函數,在JS中函數本身就是一個對象,在函數對象上實現靜態的工具方法是比較常見的現象,TypeScript定義函數類型接口並一同定義一些工具方法也可以實現,這有點像是繼承加手動解構的模式,官方文檔中的示例也是比較清晰易懂的,還是添加一些注釋的方式解析吧。
1 interface Counter { 2 (start: number): string;//定義函數接口的主體部分 3 interval: number;//定義函數的靜態屬性接口 4 reset(): void;//定義函數的靜態方法接口 5 } 6 7 function getCounter(): Counter {//這里可以說是繼承,或許它更像時通過接口定義函數的類型(它真正的含義是標識這個方法返回的值是該接口類型) 8 let counter = <Counter>function (start: number) { };//通過<接口名稱>獲取函數主體部分,並賦給一個變量(實際上這個變量才是真正實現接口的函數類型對象) 9 counter.interval = 123;//直接在函數對象上實現接口中定義的靜態屬性 10 counter.reset = function () { };//直接在函數對象上實現接口中定義的靜態方法 11 return counter;//返回這個被實現的函數 12 } 13 14 let c = getCounter(); 15 c(10); 16 c.reset(); 17 c.interval = 5.0;
接口繼承類:
當接口繼承了一個類類型時,它會繼承類的成員但不包括其實現。 就好像接口聲明了所有類中存在的成員,但並沒有提供具體實現一樣。 接口同樣會繼承到類的private和protected成員。
在實現這類接口的時候必須要同時實現繼承該接口繼承的類,或者繼承該接口繼承的類的子類,比如示例:
1 class Control { 2 private state: any; 3 } 4 5 interface SelectableControl extends Control { 6 select(): void; 7 } 8 //實現接口同時實現繼承接口繼承的類 9 class Button extends Control implements SelectableControl { 10 select() { } 11 } 12 13 class TextBox extends Control { 14 select() { } 15 } 16 //實現接口同時實現繼承接口繼承的類的子類 17 class TextBox1 extends TextBox implements SelectableControl{ 18 19 }