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

聯(lián)合類型

偶爾你會遇到這種情況,一個代碼庫希望傳入numberstring類型的參數(shù)。 例如下面的函數(shù):

/**
 * Takes a string and adds "padding" to the left.
 * If 'padding' is a string, then 'padding' is appended to the left side.
 * If 'padding' is a number, then that number of spaces is added to the left side.
 */
function padLeft(value: string, padding: any) {
    if (typeof padding === "number") {
        return Array(padding + 1).join(" ") + value;
    }
    if (typeof padding === "string") {
        return padding + value;
    }
    throw new Error(`Expected string or number, got '${padding}'.`);
}

padLeft("Hello world", 4); // returns "    Hello world"

padLeft存在一個問題,padding參數(shù)的類型指定成了any。 這就是說我們可以傳入一個既不是number也不是string類型的參數(shù),但是TypeScript卻不報錯。

let indentedString = padLeft("Hello world", true); // 編譯階段通過,運行時報錯

在傳統(tǒng)的面向?qū)ο笳Z言里,我們可能會將這兩種類型抽象成有層級的類型。 這么做顯然是非常清晰的,但同時也存在了過度設(shè)計。 padLeft原始版本的好處之一是允許我們傳入原始類型。 這做的話使用起來既方便又不過于繁鎖。 如果我們就是想使用已經(jīng)存在的函數(shù)的話,這種新的方式就不適用了。

代替any, 我們可以使用聯(lián)合類型做為padding的參數(shù):

/**
 * Takes a string and adds "padding" to the left.
 * If 'padding' is a string, then 'padding' is appended to the left side.
 * If 'padding' is a number, then that number of spaces is added to the left side.
 */
function padLeft(value: string, padding: string | number) {
    // ...
}

let indentedString = padLeft("Hello world", true); // errors during compilation

聯(lián)合類型表示一個值可以是幾種類型之一。 我們用豎線(|)分隔每個類型,所以number | string | boolean表示一個值可以是number,string,或boolean。

如果一個值是聯(lián)合類型,我們只能訪問此聯(lián)合類型的所有類型里共有的成員。

interface Bird {
    fly();
    layEggs();
}

interface Fish {
    swim();
    layEggs();
}

function getSmallPet(): Fish | Bird {
    // ...
}

let pet = getSmallPet();
pet.layEggs(); // okay
pet.swim();    // errors

這里的聯(lián)合類型可能有點復(fù)雜,但是你很容易就習(xí)慣了。 如果一個值類型是A | B,我們只能確定它具有成員同時存在于AB里。 這個例子里,Bird具有一個fly成員。 我們不能確定一個Bird | Fish類型的變量是否有fly方法。 如果變量在運行時是Fish類型,那么調(diào)用pet.fly()就出錯了。

類型保護與區(qū)分類型

聯(lián)合類型非常適合這樣的情形,可接收的值有不同的類型。 當(dāng)我們想明確地知道是否拿到Fish時會怎么做? JavaScript里常用來區(qū)分2個可能值的方法是檢查它們是否存在。 像之前提到的,我們只能訪問聯(lián)合類型的所有類型中共有的成員。

let pet = getSmallPet();

// 每一個成員訪問都會報錯
if (pet.swim) {
    pet.swim();
}
else if (pet.fly) {
    pet.fly();
}

為了讓這碼代碼工作,我們要使用類型斷言:

let pet = getSmallPet();

if ((<Fish>pet).swim) {
    (<Fish>pet).swim();
}
else {
    (<Bird>pet).fly();
}

用戶自定義的類型保護

可以注意到我們使用了多次類型斷言。 如果我們只要檢查過一次類型,就能夠在后面的每個分支里清楚pet的類型的話就好了。

TypeScript里的類型保護機制讓它成為了現(xiàn)實。 類型保護就是一些表達式,它們會在運行時檢查以確保在某個作用域里的類型。 要定義一個類型保護,我們只要簡單地定義一個函數(shù),它的返回值是一個類型斷言

function isFish(pet: Fish | Bird): pet is Fish {
    return (<Fish>pet).swim !== undefined;
}

在這個例子里,pet is Fish就是類型斷言。 一個斷言是parameterName is Type這種形式,parameterName必須是來自于當(dāng)前函數(shù)簽名里的一個參數(shù)名。

每當(dāng)使用一些變量調(diào)用isFish時,TypeScript會將變量縮減為那個具體的類型,只要這個類型與變量的原始類型是兼容的。

// 'swim' 和 'fly' 調(diào)用都沒有問題了

if (isFish(pet)) {
    pet.swim();
}
else {
    pet.fly();
}

注意TypeScript不僅知道在if分支里petFish類型; 它還清楚在else分支里,一定不是Fish類型,一定是Bird類型。

typeof類型保護

我們還沒有真正的討論過如何使用聯(lián)合類型來實現(xiàn)padLeft。 我們可以像下面這樣利用類型斷言來寫:

function isNumber(x: any): x is number {
    return typeof x === "number";
}

function isString(x: any): x is string {
    return typeof x === "string";
}

function padLeft(value: string, padding: string | number) {
    if (isNumber(padding)) {
        return Array(padding + 1).join(" ") + value;
    }
    if (isString(padding)) {
        return padding + value;
    }
    throw new Error(`Expected string or number, got '${padding}'.`);
}

然而,必須要定義一個函數(shù)來判斷類型是否是原始類型,這太痛苦了。 幸運的是,現(xiàn)在我們不必將typeof x === "number"抽象成一個函數(shù),因為TypeScript可以將它識別為一個類型保護。 也就是說我們可以直接在代碼里檢查類型了。

function padLeft(value: string, padding: string | number) {
    if (typeof padding === "number") {
        return Array(padding + 1).join(" ") + value;
    }
    if (typeof padding === "string") {
        return padding + value;
    }
    throw new Error(`Expected string or number, got '${padding}'.`);
}

這些typeof類型保護只有2個形式能被識別:typeof v === "typename"typeof v !== "typename""typename"必須是"number","string","boolean""symbol"。 但是TypeScript并不會阻止你與其它字符串比較,或者將它們位置對換,且語言不會把它們識別為類型保護。

instanceof類型保護

如果你已經(jīng)閱讀了typeof類型保護并且對JavaScript里的instanceof操作符熟悉的話,你可能已經(jīng)猜到了這節(jié)要講的內(nèi)容。

instanceof類型保護是通過其構(gòu)造函數(shù)來細化其類型。 比如,我們借鑒一下之前字符串填充的例子:

interface Padder {
    getPaddingString(): string
}

class SpaceRepeatingPadder implements Padder {
    constructor(private numSpaces: number) { }
    getPaddingString() {
        return Array(this.numSpaces + 1).join(" ");
    }
}

class StringPadder implements Padder {
    constructor(private value: string) { }
    getPaddingString() {
        return this.value;
    }
}

function getRandomPadder() {
    return Math.random() < 0.5 ?
        new SpaceRepeatingPadder(4) :
        new StringPadder("  ");
}

// 類型為SpaceRepeatingPadder | StringPadder
let padder: Padder = getRandomPadder();

if (padder instanceof SpaceRepeatingPadder) {
    padder; // 類型細化為'SpaceRepeatingPadder'
}
if (padder instanceof StringPadder) {
    padder; // 類型細化為'StringPadder'
}

instanceof的右側(cè)要求為一個構(gòu)造函數(shù),TypeScript將細化為:

  1. 這個函數(shù)的prototype屬性,如果它的類型不為any的話
  2. 類型中構(gòu)造簽名所返回的類型的聯(lián)合,順序保持一至。

交叉類型

交叉類型與聯(lián)合類型密切相關(guān),但是用法卻完全不同。 一個交叉類型,例如Person & Serializable & Loggable,同時是PersonSerializableLoggable。 就是說這個類型的對象同時擁有這三種類型的成員。 實際應(yīng)用中,你大多會在混入中見到交叉類型。 下面是一個混入的例子:

function extend<T, U>(first: T, second: U): T & U {
    let result = <T & U>{};
    for (let id in first) {
        (<any>result)[id] = (<any>first)[id];
    }
    for (let id in second) {
        if (!result.hasOwnProperty(id)) {
            (<any>result)[id] = (<any>second)[id];
        }
    }
    return result;
}

class Person {
    constructor(public name: string) { }
}
interface Loggable {
    log(): void;
}
class ConsoleLogger implements Loggable {
    log() {
        // ...
    }
}
var jim = extend(new Person("Jim"), new ConsoleLogger());
var n = jim.name;
jim.log();

類型別名

類型別名會給一個類型起個新名字。 類型別名有時和接口很像,但是可以作用于原始值,聯(lián)合類型,元組以及其它任何你需要手寫的類型。

type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): Name {
    if (typeof n === 'string') {
        return n;
    }
    else {
        return n();
    }
}

起別名不會新建一個類型 - 它創(chuàng)建了一個新名字來引用那個類型。 給原始類型起別名通常沒什么用,盡管可以做為文檔的一種形式使用。

同接口一樣,類型別名也可以是泛型 - 我們可以添加類型參數(shù)并且在別名聲明的右側(cè)傳入:

type Container<T> = { value: T };

我們也可以使用類型別名來在屬性里引用自己:

type Tree<T> = {
    value: T;
    left: Tree<T>;
    right: Tree<T>;
}

然而,類型別名不可能出現(xiàn)在聲明右側(cè)以外的地方:

type Yikes = Array<Yikes>; // 錯誤

接口 vs. 類型別名

像我們提到的,類型別名可以像接口一樣;然而,仍有一些細微差別。

一個重要區(qū)別是類型別名不能被extendsimplements也不能去extendsimplements其它類型。 因為軟件中的對象應(yīng)該對于擴展是開放的,但是對于修改是封閉的,你應(yīng)該盡量去使用接口代替類型別名。

另一方面,如果你無法通過接口來描述一個類型并且需要使用聯(lián)合類型或元組類型,這時通常會使用類型別名。

字符串字面量類型

字符串字面量類型允許你指定字符串必須的固定值。 在實際應(yīng)用中,字符串字面量類型可以與聯(lián)合類型,類型保護和類型別名很好的配合。 通過結(jié)合使用這些特性,你可以實現(xiàn)類似枚舉類型的字符串。

type Easing = "ease-in" | "ease-out" | "ease-in-out";
class UIElement {
    animate(dx: number, dy: number, easing: Easing) {
        if (easing === "ease-in") {
            // ...
        }
        else if (easing === "ease-out") {
        }
        else if (easing === "ease-in-out") {
        }
        else {
            // error! should not pass null or undefined.
        }
    }
}

let button = new UIElement();
button.animate(0, 0, "ease-in");
button.animate(0, 0, "uneasy"); // error: "uneasy" is not allowed here

你只能從三種允許的字符中選擇其一來做為參數(shù)傳遞,傳入其它值則會產(chǎn)生錯誤。

Argument of type '"uneasy"' is not assignable to parameter of type '"ease-in" | "ease-out" | "ease-in-out"'

字符串字面量類型還可以用于區(qū)分函數(shù)重載:

function createElement(tagName: "img"): HTMLImageElement;
function createElement(tagName: "input"): HTMLInputElement;
// ... more overloads ...
function createElement(tagName: string): Element {
    // ... code goes here ...
}

多態(tài)的this類型

多態(tài)的this類型表示的是某個包含類或接口的子類型。 這被稱做F-bounded多態(tài)性。 它能很容易的表現(xiàn)連貫接口間的繼承,比如。 在計算器的例子里,在每個操作之后都返回this類型:

class BasicCalculator {
    public constructor(protected value: number = 0) { }
    public currentValue(): number {
        return this.value;
    }
    public add(operand: number): this {
        this.value += operand;
        return this;
    }
    public multiply(operand: number): this {
        this.value *= operand;
        return this;
    }
    // ... other operations go here ...
}

let v = new BasicCalculator(2)
            .multiply(5)
            .add(1)
            .currentValue();

由于這個類使用了this類型,你可以繼承它,新的類可以直接使用之前的方法,不需要做任何的改變。

class ScientificCalculator extends BasicCalculator {
    public constructor(value = 0) {
        super(value);
    }
    public sin() {
        this.value = Math.sin(this.value);
        return this;
    }
    // ... other operations go here ...
}

let v = new ScientificCalculator(2)
        .multiply(5)
        .sin()
        .add(1)
        .currentValue();

如果沒有this類型,ScientificCalculator就不能夠在繼承BasicCalculator的同時還保持接口的連貫性。 multiply將會返回BasicCalculator,它并沒有sin方法。 然而,使用this類型,multiply會返回this,在這里就是ScientificCalculator