高級類型
交叉類型
交叉類型,就是將多個類型合並為一個新的類型,這個新的類型具有這多個類型的成員,含有這幾個類型的所有特性,是他們的綜合體,像是集合的並集
例子:
function extend<T,U>(first: T, second: U): T&U { let result = <T & U>{}; for (let id in first) { (<any>result)[id] = first[id]; } for (let id in second) { if (!result.hasOwnProperty(id)) { (<any>result)[id] = second[id]; } } return result; } class Person { constructor(public name: string) { } } interface Loggable { log(): void; } class myLoggable implements Loggable { log() { console.log('qwe'); } } let jim = extend(new Person('qq'), new myLoggable()); console.log(jim.name); jim.log();
例子中jim有Person中的name屬性也有myLoggable中的log()方法
聯合類型
聯合類型,不像是交叉類型是多個類型的合集,表示是這多個類型中的一種類型,像是集合中的交集,只有多個類型中共有的特性才可以被調用
例如要向一個函數傳遞參數,這個參數可能是number也可能是string
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); // " Hello world"
這里存在一個問題,將padding定義為any,表示我們可以傳遞任何值給padding,這個錯誤TypeScript是不會報錯的,只有在編譯時才會報錯
解決這個問題,可以采用聯合類型,用豎線分割每個類型,表示是這幾個類型中的一種
function padLeft(value: string, padding: string | number) { ........ } let f = padLeft("Hello world", true); // error
如果一個值是聯合類型,就只能訪問這多個類型所共有的成員
interface Bird { fly(); layEggs(); } interface Fish { swim(); layEggs(); } function getSmallPet(): Fish | Bird { ... } let pet = getSmallPet(); pet.layEggs(); // okay pet.swim(); // errors
getSmallPet的返回類型就是一個聯合類型,所以pet就只能訪問Bird和Fish的共有成員layEggs()
在上面的例子中,我們不知道pet到底是那種類型,所以就不可能去訪問哪些不是公共的成員,如果我們知道了pet的類型,就可以訪問該類型的所有成員了
類型保護和區分類型
為了解決上面提到的具體類型的確定問題,需要引入類型斷言(類型轉換)
let pet = getSmallPet(); if ((<Fish>pet).swim) { (<Fish>pet).swim(); } else { (<Bird>pet).fly(); }
問題顯而易見,每次都需要對pet進行類型轉換,麻煩
用戶自定義的類型保護
類型保護就可以解決上述每次都要進行類型斷言的缺點,類型保護就是一些表達式,它們會在運行時檢查以確保在某個作用域里的類型。 要定義一個類型保護,我們只要簡單地定義一個函數,它的返回值是一個類型斷言
function isFish(pet: Fish | Bird): pet is Fish { return (<Fish>pet).swim !== undefined; }
pet is Fish就是類型斷言。 一個斷言是 parameterName is Type這種形式,parameterName必須是來自於當前函數簽名里的一個參數名。
// 'swim' 和 'fly' 調用都沒有問題了 if (isFish(pet)) { pet.swim(); } else { pet.fly(); }
TypeScript不僅知道在if里是Fish,而且還知道在else里是Bird類型
typeof類型保護
我們將之前的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}'.`); }
如果要按這樣寫就要為每個原始類型寫一個函數,麻煩。TypeScript會把"typeof v === typeofname"和"typeof v !== typeofname"看做是類型保護,所以就不必為一個原始類型寫一個函數,直接用typeof就可以了
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}'.`); }
instanceof類型保護
instanceof類型保護是通過構造函數來細化類型的一種方式
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' }
類型別名
類型別名就是給類型起一個別名,而且可以用於基礎的數據類型
type Name = string; type NameResolver = () => string; type NameOrResolver = Name | NameResolver; function getName(n: NameOrResolver): Name { if (typeof n === 'string') { return n; } else { return n(); } }
與接口不同,類型別名不會新建一個類型,只是類型的名字變了而已
type Alias = { num: number } interface Interface { num: number; } declare function aliased(arg: Alias): Alias; declare function interfaced(arg: Interface): Interface;
在上面的代碼中interfaced返回值的類型是Interface,而aliased返回值的類型是對象字面量
類型別名與接口還有一個地方不同的是類型別名不可以被extends和implements
上面說了類型別名與接口的兩個不同點,當然也有相同點,即都可以使用泛型
type Tree<T> = { value: T; left: Tree<T>; right: Tree<T>; }
字符串字面量類型
字符串字面量類型可以與聯合類型,類型保護和類型別名很好的配合
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
只能從規定的三種類型中選擇一種類型進行傳遞,其他的類型會報錯
可辨識聯合
可以合並字符串字面量類型,聯合類型,類型保護和類型別名來創建一個叫做可辨識聯合的高級模式
先定義三個將要聯合的接口,每個接口都有kind屬性,但是值不同,king屬性可以作為可辨識的特征和標識
interface Square { kind: "square"; size: number; } interface Rectangle { kind: "rectangle"; width: number; height: number; } interface Circle { kind: "circle"; radius: number; }
然后將他們聯合到一起
type Shape = Square | Rectangle | Circle;
使用可辨識聯合
function area(s: Shape) { switch (s.kind) { case "square": return s.size * s.size; case "rectangle": return s.height * s.width; case "circle": return Math.PI * s.radius ** 2; } }
注意:如果在Shape中添加新的類型,那么在switch下也要添加相應的判斷
參考資料: