TypeScript的核心原則之一是對值所具有的shape進(jìn)行類型檢查。 它有時(shí)被稱做“鴨式辨型法”或“結(jié)構(gòu)性子類型化”。 在TypeScript里,接口的作用就是為這些類型命名和為你的代碼或第三方代碼定義契約。
下面通過一個(gè)簡單示例來觀察接口是如何工作的:
function printLabel(labelledObj: { label: string }) {
console.log(labelledObj.label);
}
let myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);
類型檢查器會(huì)查看printLabel的調(diào)用。
printLabel有一個(gè)參數(shù),并要求這個(gè)對象參數(shù)有一個(gè)名為label類型為string的屬性。
需要注意的是,我們傳入的對象參數(shù)實(shí)際上會(huì)包含很多屬性,但是編譯器只會(huì)檢查那些必需的屬性是否存在,并且其類型是否匹配。
然而,有些時(shí)候TypeScript卻并不會(huì)這么寬松,我們下面會(huì)稍做講解。
下面我們重寫上面的例子,這次使用接口來描述:必須包含一個(gè)label屬性且類型為string:
interface LabelledValue {
label: string;
}
function printLabel(labelledObj: LabelledValue) {
console.log(labelledObj.label);
}
let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);
LabelledValue接口就好比一個(gè)名字,用來描述上面例子里的要求。
它代表了有一個(gè)label屬性且類型為string的對象。
需要注意的是,我們在這里并不能像在其它語言里一樣,說傳給printLabel的對象實(shí)現(xiàn)了這個(gè)接口。我們只會(huì)去關(guān)注值的外形。
只要傳入的對象滿足上面提到的必要條件,那么它就是被允許的。
還有一點(diǎn)值得提的是,類型檢查器不會(huì)去檢查屬性的順序,只要相應(yīng)的屬性存在并且類型也是對的就可以。
接口里的屬性不全都是必需的。 有些是只在某些條件下存在,或者根本不存在。 可選屬性在應(yīng)用“option bags”模式時(shí)很常用,即給函數(shù)傳入的參數(shù)對象中只有部分屬性賦值了。
下面是應(yīng)用了“option bags”的例子:
interface SquareConfig {
color?: string;
width?: number;
}
function createSquare(config: SquareConfig): {color: string; area: number} {
let newSquare = {color: "white", area: 100};
if (config.color) {
newSquare.color = config.color;
}
if (config.width) {
newSquare.area = config.width * config.width;
}
return newSquare;
}
let mySquare = createSquare({color: "black"});
帶有可選屬性的接口與普通的接口定義差不多,只是在可選屬性名字定義的后面加一個(gè)?符號(hào)。
可選屬性的好處之一是可以對可能存在的屬性進(jìn)行預(yù)定義,好處之二是可以捕獲引用了不存在的屬性時(shí)的錯(cuò)誤。
比如,我們故意將createSquare里的color屬性名拼錯(cuò),就會(huì)得到一個(gè)錯(cuò)誤提示:
interface SquareConfig {
color?: string;
width?: number;
}
function createSquare(config: SquareConfig): { color: string; area: number } {
let newSquare = {color: "white", area: 100};
if (config.color) {
// Error: Property 'collor' does not exist on type 'SquareConfig'
newSquare.color = config.collor; // Type-checker can catch the mistyped name here
}
if (config.width) {
newSquare.area = config.width * config.width;
}
return newSquare;
}
let mySquare = createSquare({color: "black"});
我們在第一個(gè)例子里使用了接口,TypeScript讓我們傳入{ size: number; label: string; }到僅期望得到{ label: string; }的函數(shù)里。
我們已經(jīng)學(xué)過了可選屬性,并且知道他們在“option bags”模式里很有用。
然而,天真地將這兩者結(jié)合的話就會(huì)像在JavaScript里那樣搬起石頭砸自己的腳。
比如,拿createSquare例子來說:
interface SquareConfig {
color?: string;
width?: number;
}
function createSquare(config: SquareConfig): { color: string; area: number } {
// ...
}
let mySquare = createSquare({ colour: "red", width: 100 });
注意傳入createSquare的參數(shù)拼寫為colour而不是color。
在JavaScript里,這會(huì)默默地失敗。
你可能會(huì)爭辯這個(gè)程序已經(jīng)正確地類型化了,因?yàn)?code>width屬性是兼容的,不存在color屬性,而且額外的colour屬性是無意義的。
然而,TypeScript會(huì)認(rèn)為這段代碼可能存在bug。 對象字面量會(huì)被特殊對待而且會(huì)經(jīng)過額外屬性檢查,當(dāng)將它們賦值給變量或作為參數(shù)傳遞的時(shí)候。 如果一個(gè)對象字面量存在任何“目標(biāo)類型”不包含的屬性時(shí),你會(huì)得到一個(gè)錯(cuò)誤。
// error: 'colour' not expected in type 'SquareConfig'
let mySquare = createSquare({ colour: "red", width: 100 });
繞開這些檢查非常簡單。 最簡便的方法是使用類型斷言:
let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);
然而,最佳的方式是能夠添加一個(gè)字符串索引簽名,前提是你能夠確定這個(gè)對象可能具有某些做為特殊用途使用的額外屬性。
如果SquareConfig帶有上面定義的類型的color和width屬性,并且還會(huì)帶有任意數(shù)量的其它屬性,那么我們可以這樣定義它:
interface SquareConfig {
color?: string;
width?: number;
[propName: string]: any;
}
我們稍后會(huì)講到索引簽名,但在這我們要表示的是SquareConfig可以有任意數(shù)量的屬性,并且只要它們不是color和width,那么就無所謂它們的類型是什么。
還有最后一種跳過這些檢查的方式,這可能會(huì)讓你感到驚訝,它就是將這個(gè)對象賦值給一個(gè)另一個(gè)變量: 因?yàn)?code>squareOptions不會(huì)經(jīng)過額外屬性檢查,所以編譯器不會(huì)報(bào)錯(cuò)。
let squareOptions = { colour: "red", width: 100 };
let mySquare = createSquare(squareOptions);
要留意,在像上面一樣的簡單代碼里,你可能不應(yīng)該去繞開這些檢查。
對于包含方法和內(nèi)部狀態(tài)的復(fù)雜對象字面量來講,你可能需要使用這些技巧,但是大部額外屬性檢查錯(cuò)誤是真正的bug。
就是說你遇到了額外類型檢查出的錯(cuò)誤,比如選擇包,你應(yīng)該去審查一下你的類型聲明。
在這里,如果支持傳入color或colour屬性到createSquare,你應(yīng)該修改SquareConfig定義來體現(xiàn)出這一點(diǎn)。
接口能夠描述JavaScript中對象擁有的各種各樣的外形。 除了描述帶有屬性的普通對象外,接口也可以描述函數(shù)類型。
為了使用接口表示函數(shù)類型,我們需要給接口定義一個(gè)調(diào)用簽名。 它就像是一個(gè)只有參數(shù)列表和返回值類型的函數(shù)定義。參數(shù)列表里的每個(gè)參數(shù)都需要名字和類型。
interface SearchFunc {
(source: string, subString: string): boolean;
}
這樣定義后,我們可以像使用其它接口一樣使用這個(gè)函數(shù)類型的接口。 下例展示了如何創(chuàng)建一個(gè)函數(shù)類型的變量,并將一個(gè)同類型的函數(shù)賦值給這個(gè)變量。
let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
let result = source.search(subString);
if (result == -1) {
return false;
}
else {
return true;
}
}
對于函數(shù)類型的類型檢查來說,函數(shù)的參數(shù)名不需要與接口里定義的名字相匹配。 比如,我們使用下面的代碼重寫上面的例子:
let mySearch: SearchFunc;
mySearch = function(src: string, sub: string): boolean {
let result = src.search(sub);
if (result == -1) {
return false;
}
else {
return true;
}
}
函數(shù)的參數(shù)會(huì)逐個(gè)進(jìn)行檢查,要求對應(yīng)位置上的參數(shù)類型是兼容的。
如果你不想指定類型,Typescript的類型系統(tǒng)會(huì)推斷出參數(shù)類型,因?yàn)楹瘮?shù)直接賦值給了SearchFunc類型變量。
函數(shù)的返回值類型是通過其返回值推斷出來的(此例是false和true)。
如果讓這個(gè)函數(shù)返回?cái)?shù)字或字符串,類型檢查器會(huì)警告我們函數(shù)的返回值類型與SearchFunc接口中的定義不匹配。
let mySearch: SearchFunc;
mySearch = function(src, sub) {
let result = src.search(sub);
if (result == -1) {
return false;
}
else {
return true;
}
}
與使用接口描述函數(shù)類型差不多,我們也可以描述那些能夠“通過索引得到”的類型,比如a[10]或ageMap["daniel"]。
可索引類型具有一個(gè)索引簽名,它描述了對象索引的類型,還有相應(yīng)的索引返回值類型。
讓我們看一個(gè)例子:
interface StringArray {
[index: number]: string;
}
let myArray: StringArray;
myArray = ["Bob", "Fred"];
let myStr: string = myArray[0];
上面例子里,我們定義了StringArray接口,它具有索引簽名。
這個(gè)索引簽名表示了當(dāng)用number去索引StringArray時(shí)會(huì)得到string類型的返回值。
共有支持兩種索引簽名:字符串和數(shù)字。
可以同時(shí)使用兩種類型的索引,但是數(shù)字索引的返回值必須是字符串索引返回值類型的子類型。
這是因?yàn)楫?dāng)使用number來索引時(shí),JavaScript會(huì)將它轉(zhuǎn)換成string然后再去索引對象。
也就是說用100(一個(gè)number)去索引等同于使用"100"(一個(gè)string)去索引,因此兩者需要保持一致。
class Animal {
name: string;
}
class Dog extends Animal {
breed: string;
}
// Error: indexing with a 'string' will sometimes get you a Dog!
interface NotOkay {
[x: number]: Animal;
[x: string]: Dog;
}
字符串索引簽名能夠很好的描述dictionary模式,并且它們也會(huì)確保所有屬性與其返回值類型相匹配。
因?yàn)樽址饕暶髁?code>obj.property和obj["property"]兩種形式都可以。
下面的例子里,name的類型與字符串索引類型不匹配,所以類型檢查器給出一個(gè)錯(cuò)誤提示:
interface NumberDictionary {
[index: string]: number;
length: number; // 可以,length是number類型
name: string // 錯(cuò)誤,`name`的類型不是索引類型的子類型
}
與C#或Java里接口的基本作用一樣,TypeScript也能夠用它來明確的強(qiáng)制一個(gè)類去符合某種契約。
interface ClockInterface {
currentTime: Date;
}
class Clock implements ClockInterface {
currentTime: Date;
constructor(h: number, m: number) { }
}
你也可以在接口中描述一個(gè)方法,在類里實(shí)現(xiàn)它,如同下面的setTime方法一樣:
interface ClockInterface {
currentTime: Date;
setTime(d: Date);
}
class Clock implements ClockInterface {
currentTime: Date;
setTime(d: Date) {
this.currentTime = d;
}
constructor(h: number, m: number) { }
}
接口描述了類的公共部分,而不是公共和私有兩部分。 它不會(huì)幫你檢查類是否具有某些私有成員。
當(dāng)你操作類和接口的時(shí)候,你要知道類是具有兩個(gè)類型的:靜態(tài)部分的類型和實(shí)例的類型。 你會(huì)注意到,當(dāng)你用構(gòu)造器簽名去定義一個(gè)接口并試圖定義一個(gè)類去實(shí)現(xiàn)這個(gè)接口時(shí)會(huì)得到一個(gè)錯(cuò)誤:
interface ClockConstructor {
new (hour: number, minute: number);
}
class Clock implements ClockConstructor {
currentTime: Date;
constructor(h: number, m: number) { }
}
這里因?yàn)楫?dāng)一個(gè)類實(shí)現(xiàn)了一個(gè)接口時(shí),只對其實(shí)例部分進(jìn)行類型檢查。 constructor存在于類的靜態(tài)部分,所以不在檢查的范圍內(nèi)。
因此,我們應(yīng)該直接操作類的靜態(tài)部分。
看下面的例子,我們定義了兩個(gè)接口,ClockConstructor為構(gòu)造函數(shù)所用和ClockInterface為實(shí)例方法所用。
為了方便我們定義一個(gè)構(gòu)造函數(shù)createClock,它用傳入的類型創(chuàng)建實(shí)例。
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(AnalogClock, 7, 32);
因?yàn)?code>createClock的第一個(gè)參數(shù)是ClockConstructor類型,在createClock(AnalogClock, 7, 32)里,會(huì)檢查AnalogClock是否符合構(gòu)造函數(shù)簽名。
和類一樣,接口也可以相互擴(kuò)展。 這讓我們能夠從一個(gè)接口里復(fù)制成員到另一個(gè)接口里,可以更靈活地將接口分割到可重用的模塊里。
interface Shape {
color: string;
}
interface Square extends Shape {
sideLength: number;
}
let square = <Square>{};
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 = <Square>{};
square.color = "blue";
square.sideLength = 10;
square.penWidth = 5.0;
先前我們提過,接口能夠描述JavaScript里豐富的類型。 因?yàn)镴avaScript其動(dòng)態(tài)靈活的特點(diǎn),有時(shí)你會(huì)希望一個(gè)對象可以同時(shí)具有上面提到的多種類型。
一個(gè)例子就是,一個(gè)對象可以同時(shí)做為函數(shù)和對象使用,并帶有額外的屬性。
interface Counter {
(start: number): string;
interval: number;
reset(): void;
}
function getCounter(): Counter {
let counter = <Counter>function (start: number) { };
counter.interval = 123;
counter.reset = function () { };
return counter;
}
let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;
在使用JavaScript第三方庫的時(shí)候,你可能需要像上面那樣去完整地定義類型。
當(dāng)接口繼承了一個(gè)類類型時(shí),它會(huì)繼承類的成員但不包括其實(shí)現(xiàn)。 就好像接口聲明了所有類中存在的成員,但并沒有提供具體實(shí)現(xiàn)一樣。 接口同樣會(huì)繼承到類的private和protected成員。 這意味著當(dāng)你創(chuàng)建了一個(gè)接口繼承了一個(gè)擁有私有或受保護(hù)的成員的類時(shí),這個(gè)接口類型只能被這個(gè)類或其子類所實(shí)現(xiàn)(implement)。
這是很有用的,當(dāng)你有一個(gè)很深層次的繼承,但是只想你的代碼只是針對擁有特定屬性的子類起作用的時(shí)候。子類除了繼承自基類外與基類沒有任何聯(lián)系。 例:
class Control {
private state: any;
}
interface SelectableControl extends Control {
select(): void;
}
class Button extends Control {
select() { }
}
class TextBox extends Control {
select() { }
}
class Image extends Control {
}
class Location {
select() { }
}
在上面的例子里,SelectableControl包含了Control的所有成員,包括私有成員state。
因?yàn)?code>state是私有成員,所以只能夠是Control的子類們才能實(shí)現(xiàn)SelectableControl接口。
因?yàn)橹挥?code>Control的子類才能夠擁有一個(gè)聲明于Control的私有成員state,這對私有成員的兼容性是必需的。
在Control類內(nèi)部,是允許通過SelectableControl的實(shí)例來訪問私有成員state的。
實(shí)際上,SelectableControl就像Control一樣,并擁有一個(gè)select方法。
Button和TextBox類是SelectableControl的子類(因?yàn)樗鼈兌祭^承自Control并有select方法),但Image和Location類并不是這樣的。