日期:2019 年 8 月 29 日
typescript 接口
介紹
TypeScript的核心原則之一是對值所具有的結(jié)構(gòu)進行類型檢查嫌吠。 它有時被稱做“鴨式辨型法”或“結(jié)構(gòu)性子類型化”算行, 在TypeScript里垢袱,接口的作用就是為這些類型命名和為你的代碼或第三方代碼定義契約
接口初探
通過一個簡單示例來觀察接口是如何工作的:
function printLabel( labelledObj: { label: string } ){
console.log(labelledObj.label);
}
let myObj = { size: 10, label: 'hahah' };
printLabel(myObj); // 'hahah'
類型檢查器會查看printLabel的調(diào)用。 printLabel有一個參數(shù),并要求這個對象參數(shù)有一個名為label類型為string的屬性扫责。 需要注意的是,我們傳入的對象參數(shù)實際上會包含很多屬性逃呼,但是編譯器只會檢查那些必需的屬性是否存在鳖孤,并且其類型是否匹配,然而抡笼,有些時候TypeScript卻并不會這么寬松苏揣,我們下面會稍做講解。
下面我們重寫上面的例子蔫缸,這次使用接口來描述:必須包含一個label屬性且類型為string:
interface LabelledValue{
label: string;
}
function printLabel( labelledObj: LabelledValue ){
console.log(labelledObj.label);
}
let myObj = { size: 10, label: 'hahah' };
printLabel(myObj);
LabelledValue接口就好比一個名字腿准,用來描述上面例子里的要求。 它代表了有一個 label屬性且類型為string的對象。
可選屬性
接口里的屬性不全都是必需的吐葱。 有些是只在某些條件下存在街望,或者根本不存在。 可選屬性在應(yīng)用“option bags”模式時很常用弟跑,即給函數(shù)傳入的參數(shù)對象中只有部分屬性賦值了灾前。
下面是應(yīng)用了“option bags”的例子:
interface SquareConfig{
color?: string;
width?: number;
}
function createSquare( config: SquareConfig ): { color: string; area: number }{
let newSquare = { color: 'black', area: 100 };
if(config.color){
newSquare.color = config.color;
}
if(config.width){
newSquare.area = config.width * config.width;
}
return newSquare;
}
let mySquare = createSquare({ color: 'green' });
console.log(mySquare); // { color: 'green', area: 100 }
帶有可選屬性的接口與普通的接口定義差不多,只是在可選屬性名字定義的后面加一個?符號
可選屬性的好處之一是可以對可能存在的屬性進行預(yù)定義孟辑,好處之二是可以捕獲引用了不存在的屬性時的錯誤哎甲。 比如,我們故意將 createSquare里的color屬性名拼錯饲嗽,就會得到一個錯誤提示:
interface SquareConfig {
color?: string;
width?: number;
}
function createSquare(config: SquareConfig): { color: string; area: number } {
let newSquare = {color: "white", area: 100};
if (config.clor) {
// Error: Property 'clor' does not exist on type 'SquareConfig'
newSquare.color = config.clor;
}
if (config.width) {
newSquare.area = config.width * config.width;
}
return newSquare;
}
let mySquare = createSquare({color: "black"});
只讀屬性
一些對象屬性只能在對象剛剛創(chuàng)建的時候修改其值炭玫。 你可以在屬性名前用 readonly來指定只讀屬性:
interface Point{
readonly x: number;
readonly y: number;
}
let p1: Point = { x: 10, y: 20 };
p1.x = 5; // error
TypeScript具有ReadonlyArray<T>類型,它與Array<T>相似貌虾,只是把所有可變方法去掉了吞加,因此可以確保數(shù)組創(chuàng)建后再也不能被修改:
let list: number[] = [1, 2, 3];
let ro: ReadonlyArray<number> = list;
ro[0] = 2; // error
ro.push(3); // error
ro.length = 5; // error
list = ro; // error
// 上面代碼的最后一行,可以看到就算把整個ReadonlyArray賦值到一個普通數(shù)組也是不可以
// 的尽狠,但是你可以用類型斷言重寫
list = ro as number[];
readonly vs const
最簡單判斷該用readonly還是const的方法是看要把它做為變量使用還是做為一個屬性衔憨, 做為變量使用的話用 const,若做為屬性則使用readonly
額外的屬性檢查
我們已經(jīng)知道可選屬性了袄膏,下面我們依舊拿 createSquare 來舉例:
interface SquareConfig {
color?: string;
width?: number;
}
function createSquare(config: SquareConfig): { color: string; area: number } {
// ...
}
let myConfig = { coulor: 'hahah', width: 20, };
let mySquare = createSquare(myConfig); // ok
上面這種寫法不會報錯践图,但是我們換下面以種寫法:
interface SquareConfig {
color?: string;
width?: number;
}
function createSquare(config: SquareConfig): { color: string; area: number } {
// ...
}
// but 'coulor' does not exist in type 'SquareConfig'. Did you mean to write 'color'?
let mySquare = createSquare({ coulor: 'hahah', width: 20, }); // error
你可能會爭辯這個程序已經(jīng)正確地類型化了,因為width屬性是兼容的沉馆,不存在color屬性码党,而且額外的colour屬性是無意義的。然而悍及,TypeScript會認為這段代碼可能存在bug
對象字面量會被特殊對待而且會經(jīng)過 額外屬性檢查闽瓢,當將它們賦值給變量或作為參數(shù)傳遞的時候,如果一個對象字面量存在任何“目標類型”不包含的屬性時心赶,你會得到一個錯誤
如果你想繞過這些檢查扣讼,有下面三種方法:
// 方法一 最簡便的方法是使用類型斷言
let mySquare = createSquare({ coulor: 'hahah', width: 20 } as SquareConfig);
// 方法二 是將這個對象賦值給一個另一個變量: 因為 squareOptions不會經(jīng)過額外屬性檢
// 查,所以編譯器不會報錯
let squareOptions = { colour: "red", width: 100 };
let mySquare = createSquare(squareOptions);
// 方法三 最佳的方式是能夠添加一個字符串索引簽名
interface SquareConfig{
color?: string;
width?: number;
[propName: string]: any;
}
函數(shù)類型
為了使用接口表示函數(shù)類型缨叫,我們需要給接口定義一個調(diào)用簽名椭符。 它就像是一個只有參數(shù)列表和返回值類型的函數(shù)定義。參數(shù)列表里的每個參數(shù)都需要名字和類型
interface SearchFunc{
( source: string, subString: string): boolean;
}
這樣定義后耻姥,我們可以像使用其它接口一樣使用這個函數(shù)類型的接口销钝。 下例展示了如何創(chuàng)建一個函數(shù)類型的變量,并將一個同類型的函數(shù)賦值給這個變量
let mySearch: SearchFunc;
mySrach = function(src: string, sub: string): boolean{
let result = src.search(sub);
return result > -1;
}
對于函數(shù)類型的類型檢查來說琐簇,函數(shù)的參數(shù)名不需要與接口里定義的名字相匹配蒸健,所以我們可以按照上述的寫法
函數(shù)的參數(shù)會逐個進行檢查座享,要求對應(yīng)位置上的參數(shù)類型是兼容的。 如果你不想指定類型似忧,TypeScript的類型系統(tǒng)會推斷出參數(shù)類型渣叛,因為函數(shù)直接賦值給了 SearchFunc類型變量。 函數(shù)的返回值類型是通過其返回值推斷出來的(此例是 false和true)盯捌。 如果讓這個函數(shù)返回數(shù)字或字符串淳衙,類型檢查器會警告我們函數(shù)的返回值類型與 SearchFunc接口中的定義不匹配
let mySearch: SearchFunc;
mySearch = function (src, sub) {
let result = src.search(sub);
return reuslt > -1;
}
可索引的類型
與使用接口描述函數(shù)類型差不多,我們也可以描述那些能夠“通過索引得到”的類型饺著,比如a[10]或ageMap["daniel"]箫攀。 可索引類型具有一個 索引簽名,它描述了對象索引的類型幼衰,還有相應(yīng)的索引返回值類型靴跛。 讓我們看一個例子:
interface StringArray{
[index: number]: string;
}
let myArray: StringArray;
myArray = [ 'hello', 'world'];
let myStr: string = myArray[0];
TypeScript支持兩種索引簽名:字符串和數(shù)字。 可以同時使用兩種類型的索引渡嚣,但是數(shù)字索引的返回值必須是字符串索引返回值類型的子類型汤求。 這是因為當使用 number來索引時,JavaScript會將它轉(zhuǎn)換成string然后再去索引對象严拒。 也就是說用 100(一個number)去索引等同于使用"100"(一個string)去索引,因此兩者需要保持一致竖独。
class Animal{
name: string;
}
class Dog extends Annimal{
breed: string;
}
interface NotOk{ // 這種寫法不 ok
[x: number]: Animal;
[x: string]: Dog;
}
interface Ok{ // 這種很 ok
[x: number]: Dog;
[x: string]: Animal;
}
你可以將索引簽名設(shè)置為只讀裤唠,這樣就防止了給索引賦值:
interface ReadonlyStringArray{
readonly [index: number]: string;
}
let myArray: ReadonlyStringArray'
myArray = ['hello', 'world'];
myArray[0] = 'haha'; // error
類類型
實現(xiàn)接口
與C#或Java里接口的基本作用一樣,TypeScript也能夠用它來明確的強制一個類去符合某種契約
interface ClockInterface{
currentTime: Date;
}
class Clock implements ClockInterface{
currentTime: Date;
constructor( h: number, m: number){ };
}
你也可以在接口中描述一個方法莹痢,在類里實現(xiàn)它种蘸,如同下面的setTime方法一樣:
interface ClockInterface{
currentTime: date;
setTime(time: Date);
}
class Clock implements ClockInterface{
currentTime: Date;
setTime(time: Date){
this.currentTime = time;
}
constructor(h: number, m: number){ };
}
接口描述了類的公共部分,而不是公共和私有兩部分竞膳。 它不會幫你檢查類是否具有某些私有成員
類靜態(tài)部分與實例部分的區(qū)別
當你操作類和接口的時候航瞭,你要知道類是具有兩個類型的:靜態(tài)部分的類型和實例的類型。 你會注意到坦辟,當你用構(gòu)造器簽名去定義一個接口并試圖定義一個類去實現(xiàn)這個接口時會得到一個錯誤:
interface ClockConstructor {
new (hour: number, minute: number);
}
// Class 'Clock' incorrectly implements interface 'ClockConstructor'.Type 'Clock'
// provides no match for the signature 'new (hour: number, minute: number): any'.
class Clock implements ClockConstructor {
currentTime: Date;
constructor(h: number, m: number) { }
}
這里因為當一個類實現(xiàn)了一個接口時刊侯,只對其實例部分進行類型檢查。 constructor存在于類的靜態(tài)部分锉走,所以不在檢查的范圍內(nèi)
因此滨彻,我們應(yīng)該直接操作類的靜態(tài)部分。 看下面的例子挪蹭,我們定義了兩個接口亭饵, ClockConstructor為構(gòu)造函數(shù)所用和ClockInterface為實例方法所用。 為了方便我們定義一個構(gòu)造函數(shù) createClock梁厉,它用傳入的類型創(chuàng)建實例
interface ClockConstructor{
new (hour: number, minute: number) : ClockInterface;
}
interface ClockInterface{
tick();
}
function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface{
return new ctor(hour,minute);
}
class DigitalClock implements ClockInterface{
constructor(h: number, m: number){};
tick(){
console.log("beep beep");
}
}
class AnalogClock implements ClockInterface{
constructor(h: number, m: number);
tick(){
console.log("tick tock");
}
}
let digital = createClock(DigitalClock, 12, 17);
let analog = createClock(Analog, 13, 14);
因為createClock的第一個參數(shù)是ClockConstructor類型辜羊,在createClock(AnalogClock, 7, 32)里,會檢查AnalogClock是否符合構(gòu)造函數(shù)簽名
繼承接口
和類一樣,接口也可以相互繼承八秃,這讓我們能夠從一個接口里復(fù)制成員到另一個接口里碱妆,可以更靈活地將接口分割到可重用的模塊里
interface Shape{
color: string;
}
interface Square extends Shape{
length: number;
}
let mySquare = <Square>{};
mySquare.color = 'green';
mySquare.length = 20;
一個接口可以繼承多個接口,創(chuàng)建出多個接口的合成接口
interface Shape{
color: string;
}
interface PenStroke{
penWidth: number;
}
interface Square extends Shape, PenStroke{
length: number;
}
let mySquare = <Square>{};
mySquare.color = 'blue';
mySquare.penWidth = 0.5;
mySquare.length = 10;
混合類型
先前我們提過喜德,接口能夠描述JavaScript里豐富的類型山橄。 因為JavaScript其動態(tài)靈活的特點,有時你會希望一個對象可以同時具有上面提到的多種類型
一個例子就是舍悯,一個對象可以同時做為函數(shù)和對象使用航棱,并帶有額外的屬性
interface Counter{
(start: number): string;
interval: number;
reset() : void;
}
function myCounter() : Counter{
let counter = <Counter>function(start: number){}
counter.interval = 123;
counter.reset = function(){}
return counter;
}
let c = myCounter();
c(10);
c.interval = 233;
c.reset();
接口繼承類
當接口繼承了一個類類型時,它會繼承類的成員但不包括其實現(xiàn)萌衬。 就好像接口聲明了所有類中存在的成員饮醇,但并沒有提供具體實現(xiàn)一樣。 接口同樣會繼承到類的private和protected成員秕豫。 這意味著當你創(chuàng)建了一個接口繼承了一個擁有私有或受保護的成員的類時朴艰,這個接口類型只能被這個類或其子類所實現(xiàn)(implement)
當你有一個龐大的繼承結(jié)構(gòu)時這很有用,但要指出的是你的代碼只在子類擁有特定屬性時起作用混移。 這個子類除了繼承至基類外與基類沒有任何關(guān)系祠墅。 例:
class Control {
private state: any;
}
interface SelectableControl extends Control {
select(): void;
}
class Button extends Control implements SelectableControl {
select() { }
}
class TextBox extends Control {
select() { }
}
// 錯誤:“Image”類型缺少“state”屬性
class Image implements SelectableControl {
select() { }
}
// 錯誤
class Location {
}
在上面的例子里,SelectableControl包含了Control的所有成員歌径,包括私有成員state毁嗦。 因為 state是私有成員,所以只能夠是Control的子類們才能實現(xiàn)SelectableControl接口回铛。 因為只有 Control的子類才能夠擁有一個聲明于Control的私有成員state狗准,這對私有成員的兼容性是必需的
在Control類內(nèi)部,是允許通過SelectableControl的實例來訪問私有成員state的茵肃。 實際上腔长, SelectableControl接口和擁有select方法的Control類是一樣的。 Button和TextBox類是SelectableControl的子類(因為它們都繼承自Control并有select方法)验残,但Image和Location類并不是這樣的
總結(jié)與計劃
今天主要學(xué)習(xí)了 typescript 接口部分的知識捞附,就我的理解來看,接口實際上就是可以讓我們自定義某種數(shù)據(jù)類型的規(guī)范您没,它是在基礎(chǔ)數(shù)據(jù)類型的基礎(chǔ)之上做的一個更加復(fù)雜的故俐,可人為的配置的數(shù)據(jù)規(guī)范。
明天要學(xué)習(xí)的內(nèi)容是 typescript 的類紊婉,是緊接著今天的內(nèi)容繼續(xù)的药版,所以溫習(xí)一下今天的內(nèi)容差不多就可以開始看明天的東西了,加油 (ヾ(?°?°?)??) 喻犁!