交叉類型(Intersection Types)
交叉類型是將多個(gè)類型合并為一個(gè)類型。 這讓我們可以把現(xiàn)有的多種類型疊加到一起成為一種類型,它包含了所需的所有類型的特性。 例如, Person & Serializable & Loggable同時(shí)是 Person 和 Serializable 和 Loggable。 就是說這個(gè)類型的對象同時(shí)擁有了這三種類型的成員。
我們大多是在混入(mixins)或其它不適合典型面向?qū)ο竽P偷牡胤娇吹浇徊骖愋偷氖褂谩?(在JavaScript里發(fā)生這種情況的場合很多!) 下面是如何創(chuàng)建混入的一個(gè)簡單例子:
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)合類型(Union Types)
聯(lián)合類型與交叉類型很有關(guān)聯(lián),但是使用上卻完全不同。 偶爾你會(huì)遇到這種情況,一個(gè)代碼庫希望傳入 number或 string類型的參數(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存在一個(gè)問題, padding參數(shù)的類型指定成了 any。 這就是說我們可以傳入一個(gè)既不是 number也不是 string類型的參數(shù),但是TypeScript卻不報(bào)錯(cuò)。
let indentedString = padLeft("Hello world", true); // 編譯階段通過,運(yùn)行時(shí)報(bào)錯(cuò)
在傳統(tǒng)的面向?qū)ο笳Z言里,我們可能會(huì)將這兩種類型抽象成有層級(jí)的類型。 這么做顯然是非常清晰的,但同時(shí)也存在了過度設(shè)計(jì)。 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)合類型表示一個(gè)值可以是幾種類型之一。 我們用豎線( |)分隔每個(gè)類型,所以 number | string | boolean表示一個(gè)值可以是 number, string,或 boolean。
如果一個(gè)值是聯(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)合類型可能有點(diǎn)復(fù)雜,但是你很容易就習(xí)慣了。 如果一個(gè)值的類型是 A | B,我們能夠 確定的是它包含了 A 和 B中共有的成員。 這個(gè)例子里, Bird具有一個(gè) fly成員。 我們不能確定一個(gè) Bird | Fish類型的變量是否有 fly方法。 如果變量在運(yùn)行時(shí)是 Fish類型,那么調(diào)用 pet.fly()就出錯(cuò)了。
類型保護(hù)與區(qū)分類型(Type Guards and Differentiating Types)
聯(lián)合類型適合于那些值可以為不同類型的情況。 但當(dāng)我們想確切地了解是否為 Fish時(shí)怎么辦? JavaScript里常用來區(qū)分2個(gè)可能值的方法是檢查成員是否存在。 如之前提及的,我們只能訪問聯(lián)合類型中共同擁有的成員。
let pet = getSmallPet();
// 每一個(gè)成員訪問都會(huì)報(bào)錯(cuò)
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();
}
用戶自定義的類型保護(hù)
這里可以注意到我們不得不多次使用類型斷言。 假若我們一旦檢查過類型,就能在之后的每個(gè)分支里清楚地知道 pet的類型的話就好了。
TypeScript里的 類型保護(hù)機(jī)制讓它成為了現(xiàn)實(shí)。 類型保護(hù)就是一些表達(dá)式,它們會(huì)在運(yùn)行時(shí)檢查以確保在某個(gè)作用域里的類型。 要定義一個(gè)類型保護(hù),我們只要簡單地定義一個(gè)函數(shù),它的返回值是一個(gè) 類型謂詞:
function isFish(pet: Fish | Bird): pet is Fish {
? ? return (<Fish>pet).swim !== undefined;
}
在這個(gè)例子里, pet is Fish就是類型謂詞。 謂詞為 parameterName is Type這種形式, parameterName必須是來自于當(dāng)前函數(shù)簽名里的一個(gè)參數(shù)名。
每當(dāng)使用一些變量調(diào)用 isFish時(shí),TypeScript會(huì)將變量縮減為那個(gè)具體的類型,只要這個(gè)類型與變量的原始類型是兼容的。
// 'swim' 和 'fly' 調(diào)用都沒有問題了
if (isFish(pet)) {
? ? pet.swim();
}
else {
? ? pet.fly();
}
注意TypeScript不僅知道在 if分支里 pet是 Fish類型; 它還清楚在 else分支里,一定 不是 Fish類型,一定是 Bird類型。
typeof類型保護(hù)
現(xiàn)在我們回過頭來看看怎么使用聯(liá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}'.`);
}
然而,必須要定義一個(gè)函數(shù)來判斷類型是否是原始類型,這太痛苦了。 幸運(yùn)的是,現(xiàn)在我們不必將 typeof x === "number"抽象成一個(gè)函數(shù),因?yàn)門ypeScript可以將它識(shí)別為一個(gè)類型保護(hù)。 也就是說我們可以直接在代碼里檢查類型了。
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類型保護(hù)*只有兩種形式能被識(shí)別: typeof v === "typename"和 typeof v !== "typename", "typename"必須是 "number", "string", "boolean"或 "symbol"。 但是TypeScript并不會(huì)阻止你與其它字符串比較,語言不會(huì)把那些表達(dá)式識(shí)別為類型保護(hù)。
instanceof類型保護(hù)
如果你已經(jīng)閱讀了 typeof類型保護(hù)并且對JavaScript里的 instanceof操作符熟悉的話,你可能已經(jīng)猜到了這節(jié)要講的內(nèi)容。
instanceof類型保護(hù)是通過構(gòu)造函數(shù)來細(xì)化類型的一種方式。 比如,我們借鑒一下之前字符串填充的例子:
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; // 類型細(xì)化為'SpaceRepeatingPadder'
}
if (padder instanceof StringPadder) {
? ? padder; // 類型細(xì)化為'StringPadder'
}
instanceof的右側(cè)要求是一個(gè)構(gòu)造函數(shù),TypeScript將細(xì)化為:
此構(gòu)造函數(shù)的 prototype屬性的類型,如果它的類型不為 any的話
構(gòu)造簽名所返回的類型的聯(lián)合
以此順序。