TypeScript躬行記(6)——高階型別
本節將對TypeScript中型別的高階特性做詳細講解,包括交叉型別、類型別名、型別保護等。
一、交叉型別
交叉型別(Intersection Type)是將多個型別通過“&”符號合併成一個新型別,新型別將包含所有型別的特性。例如有Person和Programmer兩個類(如下程式碼所示),當man變數的型別宣告為Person&Programmer時,它就能使用兩個類的成員:name屬性和work()方法。
class Person { name: string; } class Programmer { work() { } } let man: Person&Programmer; man.name; man.work();
交叉型別常用於混入(mixin)或其它不適合典型面向物件模型的場景,例如在下面的示例中,通過交叉型別讓新物件obj同時包含a和b兩個屬性。
function extend<T, U>(first: T, second: U): T & U { const result = <T & U>{}; for (let prop in first) { (<T>result)[prop] = first[prop]; } for (let prop in second) { if (!result.hasOwnProperty(prop)) { (<U>result)[prop] = second[prop]; } } return result; } let obj = extend({ a: 1 }, { b: 2 });
二、類型別名
TypeScript提供了type關鍵字,用於建立類型別名,可作用於基本型別、聯合型別、交叉型別和泛型等任意型別,如下所示。
type Name = string; //基本型別 type Func = () => string; //函式 type Union = Name | Func; //聯合型別 type Tuple = [number, number]; //元組 type Generic<T> = { value: T }; //泛型
注意,起別名不是新建一個型別,而是提供一個可讀性更高的名稱。類型別名可在屬性裡引用自身,但不能出現在宣告的右側,如下所示。
type Tree<T> = { value: T; left: Tree<T>; right: Tree<T>; } type Arrs = Array<Arrs>; //錯誤
三、型別保護
當使用聯合型別時,只能訪問它們的公共成員。假設有一個func()函式,它的引數是由Person和Programmer兩個類組成的聯合型別,如下程式碼所示。
function func(man: Person | Programmer) { if((<Person>man).run) { (<Person>man).run(); }else { (<Programmer>man).work(); } }
雖然利用型別斷言可以確定引數型別,在編譯階段避免了報錯,但是多次呼叫型別斷言未免過於繁瑣。於是TypeScript就引入了型別保護機制,替代型別斷言。型別保護(Type Guard)是一些表示式,允許在執行時檢查型別,縮小類型範圍。
1)typeof
TypeScript可將typeof運算子識別成型別保護,從而就能直接在程式碼裡檢查型別(如下所示),其計算結果是個字串,包括“number”、“string”、“boolean”或“symbol”等關鍵字。
function send(data: number | string) { if (typeof data === "number") { //... } else if(typeof data === "string") { //... } }
2)instanceof
TypeScript也可將instanceof運算子識別成型別保護,通過建構函式來細化型別,檢測例項和類是否有關聯,如下所示。
function work(man: Person | Programmer) { if (man instanceof Person) { //... } else if(man instanceof Programmer) { //... } }
3)自定義
TypeScript還允許自定義型別保護,其形式和函式宣告類似,只是返回型別需要改成型別謂詞,如下所示。
function isPerson(man: Person | Programmer): man is Person { return !!(<Person>man).run; }
型別謂詞由當前函式的引數名稱、is關鍵字和指定的型別名稱所組成。
四、字面量型別
TypeScript可將字串字面量作為一個型別,用於指定一個字串型別的固定值。當該型別與聯合型別、類型別名等特性配合使用時,可以模擬出列舉的效果,如下所示。
type Direction = "Up" | "Down" | "Left"; function move(data: Direction) { return data; } move("Up"); //正確 move("Right"); //錯誤
move()函式只能接收Direction型別的三個固定值,傳入其它值都會產生錯誤。
字串字面量型別還可以用來區分函式過載,如下所示。
function run(data: "Left"): string; function run(data: "Down"): string; function run(data: string) { return data; }
其它常見的字面量型別還有數字和布林值,如下所示。
type Numbers = 1 | 2 | 3 | 4 | 5 | 6; type Bools = true | false;
注意,字面量型別屬於單例型別。單例型別是一種只有一個值的型別,當每個列舉成員都用字面量初始化時,列舉成員是具有型別的,叫列舉成員型別,它也屬於單例型別。
五、可辨析聯合
通過合併單例型別、聯合型別、型別保護和類型別名可建立一種高階模式:可辨析聯合(Discriminated Union),也叫做標籤聯合或代數資料型別。TypeScript中的可辨析聯合具有3個要素:
(1)具有單例型別的屬性,即可辨析的特徵或標籤。
(2)一個聯合了多個型別的類型別名。
(3)針對第一個要素中的屬性的型別保護。
在下面的示例中,首先聲明瞭兩個介面,每個介面都有字串字面量型別的kind屬性,並且其值都不同,而kind屬性就是第一個要素中的可辨析的特徵或標籤。
interface Rectangle { kind: "rectangle"; width: number; height: number; } interface Circle { kind: "circle"; radius: number; }
然後將兩個介面聯合,並建立一個類型別名,實現第二個要素,如下所示。
type Shape = Rectangle | Circle;
最後通過具有判斷性的kind屬性,結合switch語句,執行型別保護,縮小類型範圍,如下所示。
function caculate(s: Shape) { switch (s.kind) { case "rectangle": return s.height * s.width; case "circle": return Math.PI * s.radius ** 2; } }
1)完整性檢查
當未涵蓋可辨析聯合的所有變化時,需要能反饋到編譯器中。例如新增Square介面,並將它新增到Shape型別中(如下所示),如果未更新caculate()函式,那麼就不能編譯通過。
interface Square { kind: "square"; size: number; } type Shape = Rectangle | Circle | Square;
有兩種方法能實現這種預警,第一種是在輸入編譯命令時新增--strictNullChecks引數,併為caculate()函式指定返回值型別,如下所示。
function caculate(s: Shape): number { switch (s.kind) { case "rectangle": return s.height * s.width; case "circle": return Math.PI * s.radius ** 2; } }
由於switch語句沒有包含所有型別,因此TypeScript會認為該函式有可能返回undefined,從而就會編譯報錯。注意,這種方法不太精確,有很多因素(例如函式預設返回數字)會干擾完整性檢查,並且--strictNullChecks引數對舊程式碼有相容問題。
第二種方法是使用never型別,如下程式碼所示,新增一個能引發型別錯誤的assertNever()函式,並在default分支中呼叫該函式。
function assertNever(x: never): never { throw new Error("Unexpected object: " + x); } function caculate(s: Shape) { switch (s.kind) { case "rectangle": return s.height * s.width; case "circle": return Math.PI * s.radius ** 2; default: return assertNever(s); } }
雖然額外定義了一個函式,但是檢查的精確度提升了不少。
六、索引型別
索引型別(Index Type)能讓編譯器檢查使用動態屬性的場景,例如從物件中選取屬性的子集,如下所示。
function pluck(obj, names) { return names.map(n => obj[n]); }
如果要讓pluck()函式能從obj物件中成功的選出names陣列所指定的屬性,那麼需要在宣告時設定型別約束,包括names中的元素必須是obj中存在的屬性以及返回值型別得是obj屬性值的型別,下面通過泛型來描述這些約束。
function pluck<T, K extends keyof T>(obj: T, names: K[]): T[K][] { return names.map(n => obj[n]); } interface Person { name: string; age: number; } let person: Person = { name: "strick", age: 28 }; let attrs: string[] = pluck(person, ["name"]);
泛型函式pluck()引入了兩個新的型別操作符,分別是索引型別查詢操作符(keyof T)和索引訪問操作符(T[K])。前者會取T型別中由公共(public)屬性名所組成的聯合型別,例如“"name" | "age"”;後者會取T型別中指定屬性值的型別,這意味著示例中的person["name"]和Person["name"]兩者的型別都是string。
1)字串索引簽名
keyof T與T[K]同樣適用於字串索引簽名,以下面的泛型介面People為例,kType的型別是string和number的聯合型別,因為JavaScript裡的數值索引會自動轉換成字串索引;vType的型別是number,也就是索引簽名的型別。
interface People<T> { [key: string]: T; } let kType: keyof People<number>; //string | number let vType: People<number>["name"]; //number
七、對映型別
對映型別(Mapped Type)與索引型別類似,也是從現有型別中創建出一種新型別。接下來用一個例子來演示對映型別用法,假設有一個Person介面,它有兩個成員,如下所示。
interface Person { name: string; age: number; }
當需要將Person介面的每個成員都變為可選或只讀的,粗糙的解決方法是一個個的修改,如下所示。
interface PersonPartial { name?: string; age?: number; } interface PersonReadonly { readonly name: string; readonly age: number; }
而如果採用對映型別,那麼就能快速的改變介面成員,如下程式碼所示,其中Readonly<T>可將T型別的成員設為只讀,而Partial<T>會將它們設為可選。
type Readonly<T> = { readonly [P in keyof T]: T[P]; } type Partial<T> = { [P in keyof T]?: T[P]; } type PersonPartial = Partial<Person>; type ReadonlyPerson = Readonly<Person>;
[P in keyof T]的語法與索引型別的類似,但內部使用了for-in遍歷語句,其中:
(1)P是型別變數,會依次繫結到每個成員上,對應成員名的型別。
(2)T是由字串字面量構成的聯合型別,表示一組成員名,例如“"name" | "age"”。
(3)T[P]是成員值的型別。
注意,對映型別描述的是型別而非成員,如果要新增額外的成員,需要使用交叉型別的方式,如下所示,直接在型別中新增成員會無法通過編譯。
//交叉型別 type ReadonlyNew<T> = { readonly [P in keyof T]: T[P]; } & { data: boolean }; //編譯錯誤 type ReadonlyNew<T> = { readonly [P in keyof T]: T[P]; data: boolean; };
Readonly<T>和Partial<T>是一種同態轉換,即在對映時保留源型別的成員名以及其值型別,並且與目標型別相比只有修飾符有差異。而那些會建立新成員、改變成員型別或其值型別的轉換都被稱為非同態。由於Readonly<T>和Partial<T>很實用,因此它們已經被包含進TypeScript的標準庫裡,作為內建的工具型別存在。
八、條件型別
條件型別(Conditional Type)能夠表示非統一的型別對映,常以條件表示式進行型別檢測,語法類似於三目運算子,從兩個型別中選出一個,如下所示。
T extends U ? X : Y
如果T是U的子型別,那麼型別將被解析成X,否則是Y。當條件的真假無法確定時,得到的結果將是由X和Y組成的聯合型別,以下面的全域性函式sum()為例,T是布林值的子型別,當傳入的引數是true時,得到的將是string型別;而傳入false時,得到的是number型別。
declare function sum<T extends boolean>(x: T): T extends true ? string : number; let x = sum(true); //string | number
如果T或U含包含型別變數,那麼就得延遲解析,即等到型別變數都有具體型別後才能計算出條件型別的結果。在下面的示例中,建立了一個Person介面,宣告的全域性函式add()的返回值型別會根據是否是Person的子型別而改變,並且在泛型函式func()中呼叫了add()函式。
interface Person { name: string; age: number; getName(): string; } declare function add<T>(x: T): T extends Person ? string : number; function func<U>(x: U) { let a = add(x); let b: string | number = a; }
雖然a變數的型別尚不確定,但是條件型別的結果不是string就是number,因此可以成功的賦給b變數。
1)分散式條件型別
當條件型別中被檢查的型別是無型別引數(naked type parameter)時,它會被稱為分散式條件型別(Distributive Conditional Type)。其特殊之處在於它能自動分佈聯合型別,舉個簡單的例子,假設T的型別是A | B | C,那麼它會被解析成三個條件分支,如下所示。
(A | B | C) extends U ? X : Y //等同於 (A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)
分散式條件型別可以用來過濾聯合型別,如下所示,Filter<T, U>型別可從T中移除U的子型別。
type Filter<T, U> = T extends U ? never : T; type T1 = Filter<"a" | "b" | "c" | "d", "a" | "c" | "f">; // "b" | "d" type T2 = Filter<string | number | (() => void), Function>; // string | number
分散式條件型別也可與對映型別配合使用,進行鍼對性的型別對映,即不同源型別對應不同對映規則,例如對映介面的方法名,如下所示。
type FunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? K : never }[keyof T]; type T3 = FunctionPropertyNames<Person>; // "getName"
注意,條件型別與聯合型別和交叉型別相似,不允許遞迴地引用自身,下面這樣寫會在編譯階段報錯。
type Custom<T> = T extends any[] ? Custom<T[number]> : T;
2)型別推斷
在條件型別的extends子句中,允許通過infer宣告引入一個待推斷的型別變數,並且可出現多個同類型的infer宣告,例如用infer宣告來提取函式的返回值型別,如下所示。有一點要注意,只能在true分支中使用infer宣告的型別變數。
type Func<T> = T extends (...args: any[]) => infer R ? R : any;
當函式具有過載時,就取最後一個函式簽名進行推斷,如下所示,其中ReturnType<T>是內建的條件型別,可獲取函式型別T的返回值型別。
declare function load(x: string): number; declare function load(x: number): string; declare function load(x: string | number): string | number; type T4 = ReturnType<typeof load>; // string | number
注意,無法在正常型別引數的約束子語句中使用infer宣告,如下所示。
type Func<T extends (...args: any[]) => infer R> = R;
但是可以將約束裡的型別變數移除,並將其轉移到條件型別中,就能達到相同的效果,如下所示。
type AnyFunction = (...args: any[]) => any; type Func<T extends AnyFunction> = T extends (...args: any[]) => infer R ? R : any;
3)預定義的條件型別
除了之前示例中用到的ReturnType<T>之外,TypeScript還預定義了4個其它功能的條件型別,如下所列。
(1)Exclude<T, U>:從T中移除掉U的子型別。
(2)Extract<T, U>:從T中篩選出U的子型別。
(3)NonNullable<T>:從T中移除null與undefined。
(4)InstanceType<T>:獲取建構函式的例項型別。
type T11 = Exclude<"a" | "b" | "c" | "d", "a" | "c">; // "b" | "d" type T12 = Extract<"a" | "b" | "c" | "d", "a" | "c">; // "a" | "c" type T13 = NonNullable<string | number | undefined>; // string | number type T14 = ReturnType<(s: string) => void>; // void class Programmer { name: string; } type T15 = InstanceType<typeof Programmer>; //Programmer
&n