鍍金池/ 教程/ HTML/ 介紹
初始化項目結構
聯(lián)合類型
介紹
介紹
介紹
編譯選項
TypeScript 1.6
介紹
介紹
發(fā)展路線圖
介紹
在MSBuild里使用編譯選項
可迭代性
TypeScript 1.3
介紹
介紹
TypeScript 1.1
變量聲明
即將到來的Angular 2框架是使用TypeScript開發(fā)的。 因此Angular和TypeScript一起使用非常簡單方便
tsconfig.json
介紹
介紹
介紹
在MSBuild里使用編譯選項
使用TypeScript的每日構建版本
新建工程
枚舉
三斜線指令
結合ASP.NET v5使用TypeScript
TypeScript里的this
介紹
TypeScript 1.4
編碼規(guī)范
介紹
模塊解析
ASP.NET 4
架構概述
介紹
介紹
ASP.NET Core
TypeScript 1.8
介紹
介紹
創(chuàng)建簡單工程
TypeScript 1.7
TypeScript 1.5
NPM包的類型
支持TypeScript的編輯器

介紹

TypeScript的核心原則之一是對值所具有的shape進行類型檢查。 它有時被稱做“鴨式辨型法”或“結構性子類型化”。 在TypeScript里,接口的作用就是為這些類型命名和為你的代碼或第三方代碼定義契約。

接口初探

下面通過一個簡單示例來觀察接口是如何工作的:

function printLabel(labelledObj: { label: string }) {
  console.log(labelledObj.label);
}

let myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);

類型檢查器會查看printLabel的調用。 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: "Size 10 Object"};
printLabel(myObj);

LabelledValue接口就好比一個名字,用來描述上面例子里的要求。 它代表了有一個label屬性且類型為string的對象。 需要注意的是,我們在這里并不能像在其它語言里一樣,說傳給printLabel的對象實現(xiàn)了這個接口。我們只會去關注值的外形。 只要傳入的對象滿足上面提到的必要條件,那么它就是被允許的。

還有一點值得提的是,類型檢查器不會去檢查屬性的順序,只要相應的屬性存在并且類型也是對的就可以。

可選屬性

接口里的屬性不全都是必需的。 有些是只在某些條件下存在,或者根本不存在。 可選屬性在應用“option bags”模式時很常用,即給函數(shù)傳入的參數(shù)對象中只有部分屬性賦值了。

下面是應用了“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.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"});

額外的屬性檢查

我們在第一個例子里使用了接口,TypeScript讓我們傳入{ size: number; label: string; }到僅期望得到{ label: string; }的函數(shù)里。 我們已經(jīng)學過了可選屬性,并且知道他們在“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的參數(shù)拼寫為colour而不是color。 在JavaScript里,這會默默地失敗。

你可能會爭辯這個程序已經(jīng)正確地類型化了,因為width屬性是兼容的,不存在color屬性,而且額外的colour屬性是無意義的。

然而,TypeScript會認為這段代碼可能存在bug。 對象字面量會被特殊對待而且會經(jīng)過額外屬性檢查,當將它們賦值給變量或作為參數(shù)傳遞的時候。 如果一個對象字面量存在任何“目標類型”不包含的屬性時,你會得到一個錯誤。

// 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帶有上面定義的類型的colorwidth屬性,并且還會帶有任意數(shù)量的其它屬性,那么我們可以這樣定義它:

interface SquareConfig {
    color?: string;
    width?: number;
    [propName: string]: any;
}

我們稍后會講到索引簽名,但在這我們要表示的是SquareConfig可以有任意數(shù)量的屬性,并且只要它們不是colorwidth,那么就無所謂它們的類型是什么。

還有最后一種跳過這些檢查的方式,這可能會讓你感到驚訝,它就是將這個對象賦值給一個另一個變量: 因為squareOptions不會經(jīng)過額外屬性檢查,所以編譯器不會報錯。

let squareOptions = { colour: "red", width: 100 };
let mySquare = createSquare(squareOptions);

要留意,在像上面一樣的簡單代碼里,你可能不應該去繞開這些檢查。 對于包含方法和內(nèi)部狀態(tài)的復雜對象字面量來講,你可能需要使用這些技巧,但是大部額外屬性檢查錯誤是真正的bug。 就是說你遇到了額外類型檢查出的錯誤,比如選擇包,你應該去審查一下你的類型聲明。 在這里,如果支持傳入colorcolour屬性到createSquare,你應該修改SquareConfig定義來體現(xiàn)出這一點。

函數(shù)類型

接口能夠描述JavaScript中對象擁有的各種各樣的外形。 除了描述帶有屬性的普通對象外,接口也可以描述函數(shù)類型。

為了使用接口表示函數(shù)類型,我們需要給接口定義一個調用簽名。 它就像是一個只有參數(shù)列表和返回值類型的函數(shù)定義。參數(shù)列表里的每個參數(shù)都需要名字和類型。

interface SearchFunc {
  (source: string, subString: string): boolean;
}

這樣定義后,我們可以像使用其它接口一樣使用這個函數(shù)類型的接口。 下例展示了如何創(chuàng)建一個函數(shù)類型的變量,并將一個同類型的函數(shù)賦值給這個變量。

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ù)會逐個進行檢查,要求對應位置上的參數(shù)類型是兼容的。 如果你不想指定類型,Typescript的類型系統(tǒng)會推斷出參數(shù)類型,因為函數(shù)直接賦值給了SearchFunc類型變量。 函數(shù)的返回值類型是通過其返回值推斷出來的(此例是falsetrue)。 如果讓這個函數(shù)返回數(shù)字或字符串,類型檢查器會警告我們函數(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"]。 可索引類型具有一個索引簽名,它描述了對象索引的類型,還有相應的索引返回值類型。 讓我們看一個例子:

interface StringArray {
  [index: number]: string;
}

let myArray: StringArray;
myArray = ["Bob", "Fred"];

let myStr: string = myArray[0];

上面例子里,我們定義了StringArray接口,它具有索引簽名。 這個索引簽名表示了當用number去索引StringArray時會得到string類型的返回值。

共有支持兩種索引簽名:字符串和數(shù)字。 可以同時使用兩種類型的索引,但是數(shù)字索引的返回值必須是字符串索引返回值類型的子類型。 這是因為當使用number來索引時,JavaScript會將它轉換成string然后再去索引對象。 也就是說用100(一個number)去索引等同于使用"100"(一個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模式,并且它們也會確保所有屬性與其返回值類型相匹配。 因為字符串索引聲明了obj.propertyobj["property"]兩種形式都可以。 下面的例子里,name的類型與字符串索引類型不匹配,所以類型檢查器給出一個錯誤提示:

interface NumberDictionary {
  [index: string]: number;
  length: number;    // 可以,length是number類型
  name: string       // 錯誤,`name`的類型不是索引類型的子類型
}

類類型

實現(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(d: Date);
}

class Clock implements ClockInterface {
    currentTime: Date;
    setTime(d: Date) {
        this.currentTime = d;
    }
    constructor(h: number, m: number) { }
}

接口描述了類的公共部分,而不是公共和私有兩部分。 它不會幫你檢查類是否具有某些私有成員。

類靜態(tài)部分與實例部分的區(qū)別

當你操作類和接口的時候,你要知道類是具有兩個類型的:靜態(tài)部分的類型和實例的類型。 你會注意到,當你用構造器簽名去定義一個接口并試圖定義一個類去實現(xiàn)這個接口時會得到一個錯誤:

interface ClockConstructor {
    new (hour: number, minute: number);
}

class Clock implements ClockConstructor {
    currentTime: Date;
    constructor(h: number, m: number) { }
}

這里因為當一個類實現(xiàn)了一個接口時,只對其實例部分進行類型檢查。 constructor存在于類的靜態(tài)部分,所以不在檢查的范圍內(nèi)。

因此,我們應該直接操作類的靜態(tài)部分。 看下面的例子,我們定義了兩個接口,ClockConstructor為構造函數(shù)所用和ClockInterface為實例方法所用。 為了方便我們定義一個構造函數(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(AnalogClock, 7, 32);

因為createClock的第一個參數(shù)是ClockConstructor類型,在createClock(AnalogClock, 7, 32)里,會檢查AnalogClock是否符合構造函數(shù)簽名。

擴展接口

和類一樣,接口也可以相互擴展。 這讓我們能夠從一個接口里復制成員到另一個接口里,可以更靈活地將接口分割到可重用的模塊里。

interface Shape {
    color: string;
}

interface Square extends Shape {
    sideLength: number;
}

let square = <Square>{};
square.color = "blue";
square.sideLength = 10;

一個接口可以繼承多個接口,創(chuàng)建出多個接口的合成接口。

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其動態(tài)靈活的特點,有時你會希望一個對象可以同時具有上面提到的多種類型。

一個例子就是,一個對象可以同時做為函數(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第三方庫的時候,你可能需要像上面那樣去完整地定義類型。

接口繼承類

當接口繼承了一個類類型時,它會繼承類的成員但不包括其實現(xiàn)。 就好像接口聲明了所有類中存在的成員,但并沒有提供具體實現(xiàn)一樣。 接口同樣會繼承到類的private和protected成員。 這意味著當你創(chuàng)建了一個接口繼承了一個擁有私有或受保護的成員的類時,這個接口類型只能被這個類或其子類所實現(xiàn)(implement)。

這是很有用的,當你有一個很深層次的繼承,但是只想你的代碼只是針對擁有特定屬性的子類起作用的時候。子類除了繼承自基類外與基類沒有任何聯(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。 因為state是私有成員,所以只能夠是Control的子類們才能實現(xiàn)SelectableControl接口。 因為只有Control的子類才能夠擁有一個聲明于Control的私有成員state,這對私有成員的兼容性是必需的。

Control類內(nèi)部,是允許通過SelectableControl的實例來訪問私有成員state的。 實際上,SelectableControl就像Control一樣,并擁有一個select方法。 ButtonTextBox類是SelectableControl的子類(因為它們都繼承自Control并有select方法),但ImageLocation類并不是這樣的。

上一篇:發(fā)展路線圖下一篇:TypeScript 1.6