偶爾你會遇到這種情況,一個代碼庫希望傳入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
存在一個問題,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
,我們只能確定它具有成員同時存在于A
和B
里。
這個例子里,Bird
具有一個fly
成員。
我們不能確定一個Bird | Fish
類型的變量是否有fly
方法。
如果變量在運行時是Fish
類型,那么調(diào)用pet.fly()
就出錯了。
聯(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
分支里pet
是Fish
類型;
它還清楚在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將細化為:
prototype
屬性,如果它的類型不為any
的話交叉類型與聯(lián)合類型密切相關(guān),但是用法卻完全不同。
一個交叉類型,例如Person & Serializable & Loggable
,同時是Person
和Serializable
和Loggable
。
就是說這個類型的對象同時擁有這三種類型的成員。
實際應(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>; // 錯誤
像我們提到的,類型別名可以像接口一樣;然而,仍有一些細微差別。
一個重要區(qū)別是類型別名不能被extends
和implements
也不能去extends
和implements
其它類型。
因為軟件中的對象應(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 ...
}
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
。