TypeScript介面
介紹
TypeScript的核心原則之一是對值所具有的結構進行型別檢查。 它有時被稱做“鴨式辨型法”或“結構性子型別化”。 在TypeScript裡,介面的作用就是為這些型別命名和為你的程式碼或第三方程式碼定義契約。
介面的作用類似於抽象類,不同點在於:介面中的所有方法和屬性都是沒有實值的,換句話說介面中的所有方法都是抽象方法;
介面主要負責定義一個類的結構,介面可以去限制一個物件的介面:物件只有包含介面中定義的所有屬性和方法時才能匹配介面;
同時,可以讓一個類去實現介面,實現介面時類中要保護介面中的所有屬性.
介面初探
下面通過一個簡單示例來觀察介面是如何工作的:
function printLabel(labelledObj: { label: string }) { console.log(labelledObj.label); } let myObj = { size: 10, label: "Size 10 Object" }; printLabel(myObj);
型別檢查器會檢視printLabel
的呼叫。 printLabel
有一個引數,並要求這個物件引數有一個名為label
型別為string
的屬性。 需要注意的是,我們傳入的物件引數實際上會包含很多屬性,但是編譯器只會檢查那些必需的屬性是否存在,並且其型別是否匹配。 然而,有些時候TypeScript卻並不會這麼寬鬆,我們下面會稍做講解。
下面我們重寫上面的例子,這次使用介面來描述:必須包含一個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
介面就好比一個名字,用來描述上面例子裡的要求。 它代表了有一個 label
屬性且型別為string
的物件。 需要注意的是,我們在這裡並不能像在其它語言裡一樣,說傳給 printLabel
的物件實現了這個介面。我們只會去關注值的外形。 只要傳入的物件滿足上面提到的必要條件,那麼它就是被允許的。
還有一點值得提的是,型別檢查器不會去檢查屬性的順序,只要相應的屬性存在並且型別也是對的就可以。
可選屬性
接口裡的屬性不全都是必需的。 有些是隻在某些條件下存在,或者根本不存在。 可選屬性在應用“option bags”模式時很常用,即給函式傳入的引數物件中只有部分屬性賦值了。
下面是應用了“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"});
帶有可選屬性的介面與普通的介面定義差不多,只是在可選屬性名字定義的後面加一個?
符號。
可選屬性的好處之一是可以對可能存在的屬性進行預定義,好處之二是可以捕獲引用了不存在的屬性時的錯誤。 比如,我們故意將 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"});
只讀屬性
一些物件屬性只能在物件剛剛建立的時候修改其值。 你可以在屬性名前用 readonly
來指定只讀屬性:
interface Point {
readonly x: number;
readonly y: number;
}
你可以通過賦值一個物件字面量來構造一個Point
。 賦值後, x
和y
再也不能被改變了。
let p1: Point = { x: 10, y: 20 };
p1.x = 5; // error!
TypeScript具有ReadonlyArray
型別,它與Array
相似,只是把所有可變方法去掉了,因此可以確保陣列建立後再也不能被修改:
let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // error!
ro.push(5); // error!
ro.length = 100; // error!
a = ro; // error!
上面程式碼的最後一行,可以看到就算把整個ReadonlyArray
賦值到一個普通陣列也是不可以的。 但是你可以用型別斷言重寫:
a = ro as number[];
readonly
vs const
最簡單判斷該用readonly
還是const
的方法是看要把它做為變數使用還是做為一個屬性。 做為變數使用的話用 const
,若做為屬性則使用readonly
。
額外的屬性檢查
我們在第一個例子裡使用了介面,TypeScript讓我們傳入{ size: number; label: string; }
到僅期望得到{ label: string; }
的函式裡。 我們已經學過了可選屬性,並且知道他們在“option bags”模式裡很有用。
然而,天真地將這兩者結合的話就會像在JavaScript裡那樣搬起石頭砸自己的腳。 比如,拿 createSquare
例子來說:
interface SquareConfig {
color?: string;
width?: number;
}
function createSquare(config: SquareConfig): { color: string; area: number } {
// ...
}
let mySquare = createSquare({ colour: "red", width: 100 });
注意傳入createSquare
的引數拼寫為colour
而不是color
。 在JavaScript裡,這會默默地失敗。
你可能會爭辯這個程式已經正確地型別化了,因為width
屬性是相容的,不存在color
屬性,而且額外的colour
屬性是無意義的。
然而,TypeScript會認為這段程式碼可能存在bug。 物件字面量會被特殊對待而且會經過 額外屬性檢查,當將它們賦值給變數或作為引數傳遞的時候。 如果一個物件字面量存在任何“目標型別”不包含的屬性時,你會得到一個錯誤。
// error: 'colour' not expected in type 'SquareConfig'
let mySquare = createSquare({ colour: "red", width: 100 });
繞開這些檢查非常簡單。 最簡便的方法是使用型別斷言:
let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);
然而,最佳的方式是能夠新增一個字串索引簽名,前提是你能夠確定這個物件可能具有某些做為特殊用途使用的額外屬性。 如果 SquareConfig
帶有上面定義的型別的color
和width
屬性,並且還會帶有任意數量的其它屬性,那麼我們可以這樣定義它:
interface SquareConfig {
color?: string;
width?: number;
[propName: string]: any;
}
我們稍後會講到索引簽名,但在這我們要表示的是SquareConfig
可以有任意數量的屬性,並且只要它們不是color
和width
,那麼就無所謂它們的型別是什麼。
還有最後一種跳過這些檢查的方式,這可能會讓你感到驚訝,它就是將這個物件賦值給一個另一個變數: 因為 squareOptions
不會經過額外屬性檢查,所以編譯器不會報錯。
let squareOptions = { colour: "red", width: 100 };
let mySquare = createSquare(squareOptions);
要留意,在像上面一樣的簡單程式碼裡,你可能不應該去繞開這些檢查。 對於包含方法和內部狀態的複雜物件字面量來講,你可能需要使用這些技巧,但是大部額外屬性檢查錯誤是真正的bug。 就是說你遇到了額外型別檢查出的錯誤,比如“option bags”,你應該去審查一下你的型別宣告。 在這裡,如果支援傳入 color
或colour
屬性到createSquare
,你應該修改SquareConfig
定義來體現出這一點。
函式型別
介面能夠描述JavaScript中物件擁有的各種各樣的外形。 除了描述帶有屬性的普通物件外,介面也可以描述函式型別。
為了使用介面表示函式型別,我們需要給介面定義一個呼叫簽名。 它就像是一個只有引數列表和返回值型別的函式定義。引數列表裡的每個引數都需要名字和型別。
interface SearchFunc {
(source: string, subString: string): boolean;
}
這樣定義後,我們可以像使用其它介面一樣使用這個函式型別的介面。 下例展示瞭如何建立一個函式型別的變數,並將一個同類型的函式賦值給這個變數。
let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
let result = source.search(subString);
return result > -1;
}
對於函式型別的型別檢查來說,函式的引數名不需要與接口裡定義的名字相匹配。 比如,我們使用下面的程式碼重寫上面的例子:
let mySearch: SearchFunc;
mySearch = function(src: string, sub: string): boolean {
let result = src.search(sub);
return result > -1;
}
函式的引數會逐個進行檢查,要求對應位置上的引數型別是相容的。 如果你不想指定型別,TypeScript的型別系統會推斷出引數型別,因為函式直接賦值給了 SearchFunc
型別變數。 函式的返回值型別是通過其返回值推斷出來的(此例是 false
和true
)。 如果讓這個函式返回數字或字串,型別檢查器會警告我們函式的返回值型別與 SearchFunc
介面中的定義不匹配。
let mySearch: SearchFunc;
mySearch = function(src, sub) {
let result = src.search(sub);
return result > -1;
}
可索引的型別
與使用介面描述函式型別差不多,我們也可以描述那些能夠“通過索引得到”的型別,比如a[10]
或ageMap["daniel"]
。 可索引型別具有一個 索引簽名,它描述了物件索引的型別,還有相應的索引返回值型別。 讓我們看一個例子:
interface StringArray {
[index: number]: string;
}
let myArray: StringArray;
myArray = ["Bob", "Fred"];
let myStr: string = myArray[0];
上面例子裡,我們定義了StringArray
介面,它具有索引簽名。 這個索引簽名表示了當用 number
去索引StringArray
時會得到string
型別的返回值。
TypeScript支援兩種索引簽名:字串和數字。 可以同時使用兩種型別的索引,但是數字索引的返回值必須是字串索引返回值型別的子型別。 這是因為當使用 number
來索引時,JavaScript會將它轉換成string
然後再去索引物件。 也就是說用 100
(一個number
)去索引等同於使用"100"
(一個string
)去索引,因此兩者需要保持一致。
class Animal {
name: string;
}
class Dog extends Animal {
breed: string;
}
// 錯誤:使用數值型的字串索引,有時會得到完全不同的Animal!
interface NotOkay {
[x: number]: Animal;
[x: string]: Dog;
}
字串索引簽名能夠很好的描述dictionary
模式,並且它們也會確保所有屬性與其返回值型別相匹配。 因為字串索引聲明瞭 obj.property
和obj["property"]
兩種形式都可以。 下面的例子裡, name
的型別與字串索引型別不匹配,所以型別檢查器給出一個錯誤提示:
interface NumberDictionary {
[index: string]: number;
length: number; // 可以,length是number型別
name: string // 錯誤,`name`的型別與索引型別返回值的型別不匹配
}
最後,你可以將索引簽名設定為只讀,這樣就防止了給索引賦值:
interface ReadonlyStringArray {
readonly [index: number]: string;
}
let myArray: ReadonlyStringArray = ["Alice", "Bob"];
myArray[2] = "Mallory"; // error!
你不能設定myArray[2]
,因為索引簽名是隻讀的。
類型別
實現介面
與C#或Java裡介面的基本作用一樣,TypeScript也能夠用它來明確的強制一個類去符合某種契約。
interface ClockInterface {
currentTime: Date;
}
class Clock implements ClockInterface {
currentTime: Date;
constructor(h: number, m: number) { }
}
你也可以在介面中描述一個方法,在類裡實現它,如同下面的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) { }
}
介面描述了類的公共部分,而不是公共和私有兩部分。 它不會幫你檢查類是否具有某些私有成員。
類靜態部分與例項部分的區別
當你操作類和介面的時候,你要知道類是具有兩個型別的:靜態部分的型別和例項的型別。 你會注意到,當你用構造器簽名去定義一個介面並試圖定義一個類去實現這個介面時會得到一個錯誤:
interface ClockConstructor {
new (hour: number, minute: number);
}
class Clock implements ClockConstructor {
currentTime: Date;
constructor(h: number, m: number) { }
}
這裡因為當一個類實現了一個介面時,只對其例項部分進行型別檢查。 constructor存在於類的靜態部分,所以不在檢查的範圍內。
因此,我們應該直接操作類的靜態部分。 看下面的例子,我們定義了兩個介面, ClockConstructor
為建構函式所用和ClockInterface
為例項方法所用。 為了方便我們定義一個建構函式 createClock
,它用傳入的型別建立例項。
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);
因為createClock
的第一個引數是ClockConstructor
型別,在createClock(AnalogClock, 7, 32)
裡,會檢查AnalogClock
是否符合建構函式簽名。
繼承介面
和類一樣,介面也可以相互繼承。 這讓我們能夠從一個接口裡複製成員到另一個接口裡,可以更靈活地將介面分割到可重用的模組裡。
interface Shape {
color: string;
}
interface Square extends Shape {
sideLength: number;
}
let square = <Square>{};
square.color = "blue";
square.sideLength = 10;
一個介面可以繼承多個介面,創建出多個介面的合成介面。
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裡豐富的型別。 因為JavaScript其動態靈活的特點,有時你會希望一個物件可以同時具有上面提到的多種型別。
一個例子就是,一個物件可以同時做為函式和物件使用,並帶有額外的屬性。
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第三方庫的時候,你可能需要像上面那樣去完整地定義型別。
介面繼承類
當介面繼承了一個類型別時,它會繼承類的成員但不包括其實現。 就好像介面聲明瞭所有類中存在的成員,但並沒有提供具體實現一樣。 介面同樣會繼承到類的private和protected成員。 這意味著當你建立了一個介面繼承了一個擁有私有或受保護的成員的類時,這個介面型別只能被這個類或其子類所實現(implement)。
當你有一個龐大的繼承結構時這很有用,但要指出的是你的程式碼只在子類擁有特定屬性時起作用。 這個子類除了繼承至基類外與基類沒有任何關係。 例:
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
的子類們才能實現SelectableControl
介面。 因為只有 Control
的子類才能夠擁有一個聲明於Control
的私有成員state
,這對私有成員的相容性是必需的。
在Control
類內部,是允許通過SelectableControl
的例項來訪問私有成員state
的。 實際上, SelectableControl
介面和擁有select
方法的Control
類是一樣的。 Button
和TextBox
類是SelectableControl
的子類(因為它們都繼承自Control
並有select
方法),但Image
和Location
類並不是這樣的。