⒈交叉類型(Intersection Types)
交叉類型是將多個類型合並為一個類型。 這讓我們可以把現有的多種類型疊加到一起成為一種類型,它包含了所需的所有類型的特性。 例如, Person & Serializable & Loggable
同時是 Person
和 Serializable
和 Loggable
。 就是說這個類型的對象同時擁有了這三種類型的成員。
每當我們正確的使用交叉類型的時候,TypeScript可以幫我們合理地將兩個不同類型疊加為新的類型,並包含了所需的所有類型。
我們大多是在混入(mixins)或其它不適合典型面向對象模型的地方看到交叉類型的使用。 (在JavaScript里發生這種情況的場合很多!)
type newType = number & string; let a : newType; interface A{ a:number, b:string, } interface B{ c:string, d:string, } type newType2 = A & B; let b : newType2;
這里的Type關鍵字是用來聲明類型變量的。在運行時,與類型相關的代碼都會被移除掉,並不會影響到JavaScript的執行。
*當交叉類型中有屬性沖突時,則無論如何賦值都不可能通過類型檢查。如下面的代碼所示:
interface A{ a:number, b:string, } interface B{ c:string, d:string, } type newType = A & B; let a : newType = {a:1,b:'',c:'',d:''}; a.a = 1; a.b = ''; a.c = ''; a.d = 5; //Error,無法通過類型檢查
⒉聯合類型(Union Types)
聯合類型與交叉類型類似,但使用上卻完全不同。
例如我們需要一個變量可能是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); // returns " Hello world"
padLeft函數
存在一個問題, padding
參數的類型指定為 any
。 也就是說,我們可以傳入一個既不是 number
也不是 string
類型的參數,但是TypeScript卻不報錯。
let indentedString = padLeft("Hello world", true); // 編譯階段通過,運行時報錯
在傳統的面向對象語言里,我們可以使用重載或將這兩種類型抽象成有層級的類型(父類與子類)。 這么做顯然是非常清晰的,但同時也存在了過度設計。
因為在JavaScript中並沒有重載可以使用(可以使用特殊的方式創建出類似重載的函數),因此在JavaScript的函數中手動去判斷參數的類型這種操作更為常見,這在一定程度上避免了過度設計。
padLeft
原始版本的好處之一是允許我們傳入原始類型。 這樣做的話使用起來既簡單又方便。 如果我們就是想使用已經存在的函數的話,這種新的方式就不適用了。
如果我們希望更准確的描述padding的類型,就可以使用聯合類型將padding的類型限定為既可以是number又可以是string。
代替 any
, 我們可以使用 聯合類型作為 padding
的參數:
function padLeft(value: string, padding: string | number) { // ... } let indentedString = padLeft("Hello world", true); // 編譯器報錯,類型true的參數不能賦值給類型string|number的參數
聯合類型表示一個變量可以是幾種類型之一。 我們用豎線( |
)分隔每個類型,所以 number | string | boolean
表示一個值可以是 number
、string
或 boolean
。
注意,如果一個值是聯合類型,我們只能訪問它們共有的屬性或方法。
我們來看一下下面的例子:
interface Bird { fly(); layEggs(); } interface Fish { swim(); layEggs(); } function getSmallPet(): Fish | Bird { // ... } let pet = getSmallPet(); pet.layEggs(); // okay pet.swim(); // errors
如果一個值的類型是 A | B
,我們能夠 確定的是它包含了 A
和 B
中共有的成員。 這個例子里, Bird
具有一個 fly
成員。 我們不能確定一個 Bird | Fish
類型的變量是否有 fly
方法。 如果變量在運行時是 Fish
類型,那么調用 pet.fly()
就出錯了。
聯合類型取的是交集,交叉類型取的是並集,這聽起來和它們的名字有些沖突。
**謹記,TypeScript只會幫你在編譯時做類型檢查,並不確保你的代碼在運行過程中的安全。
⒊類型保護【區分值的類型】
聯合類型適合於那些值可以為不同類型的情況。 但當我們想確切地了解某個值的類型時該怎么辦? JavaScript里常用來區分2個可能值的方法是檢查成員是否存在。 如之前提及的,我們只能訪問聯合類型中共同擁有的成員。
let pet = getSmallPet(); // 每一個成員訪問都會報錯 if (pet.swim) { pet.swim(); } else if (pet.fly) { pet.fly(); }
而在TypeScript中,我們可以使用類型斷言。
let pet = getSmallPet(); if ((<Fish>pet).swim) { (<Fish>pet).swim(); } else { (<Bird>pet).fly(); }
為了准確判斷值的類型,我們在方法體中多次使用了類型斷言(即使通過了類型斷言,我們知道了值的類型,在接下來的代碼中,我們仍然要對其添加類型斷言),這是一件非常麻煩的事情。如果我們一旦檢查並確定了值的類型,在之后的代碼中無需類型斷言就能清楚地知道值的類型的話就好了。
1.自定義類型保護(用戶自定義的類型保護)
TypeScript里的 類型保護機制讓它成為了現實。 類型保護就是一些表達式,它們會在運行時檢查以確保在某個作用域里的類型。 既使可讀性得到提升,又減少了使用煩瑣的類型斷言。要定義一個類型保護,我們只需要簡單地定義一個函數就可以,但返回值是一個主謂賓語句( 類型謂詞),如下所示:
function isFish(pet: Fish | Bird): pet is Fish { return (<Fish>pet).swim !== undefined; }
在這個例子里, pet is Fish
就是類型謂詞。 謂詞為 parameterName is Type
這種形式, parameterName
必須是來自於當前函數簽名里的一個參數名。
每當使用一些變量調用 isFish
時,TypeScript會將變量指定為類型保護中的類型,只要這個類型與變量的原始類型是兼容的。
// 'swim' 和 'fly' 調用都沒有問題了 if (isFish(pet)) { pet.swim(); } else { pet.fly(); }
注意,TypeScript不僅知道在 if
分支里 pet
是 Fish
類型; 它還清楚在 else
分支里pet一定是 Bird
類型,這得益於類型保護的實現。
2.typeof
類型保護
現在我們可以使用類型保護來重構一開始的padLeft代碼了,可以考慮用聯合類型書寫 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}'.`); }
然而,每次typeof進行類型判斷都必須要定義一個函數,這太痛苦了。 幸運的是,現在我們不必將 typeof x === "number"
抽象成一個函數,因為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
類型保護只有兩種形式能被識別: typeof v === "typename"
和 typeof v !== "typename"
,且typeof在TypeScript中使用時,只有匹配基本類型時(即"typename"
必須是 "number"
, "string"
, "boolean"
或 "symbol"
),才會啟動類型保護 。 但是TypeScript並不會阻止你與其它字符串比較(例如typeof v === 'hello TypeScript'),typeof並不會把它識別為一個有效的類型,因此也不會把這些字符串識別為類型保護。
3.instanceof
類型保護
除了typeof以外,instanceof也可以起來類型保護的作用。Instanceof相較於typeof,其類型保護更為精細,是通過構造函數來區分類型的一種方式。
如果你已經閱讀了 typeof
類型保護並且對JavaScript里的 instanceof
操作符熟悉的話,你可能已經很輕而易舉的在TypeScript中使用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' }
可以看出instanceof在類型的使用上,與typeof相比,可以將類作為比較對象,從而實現類型保護。
instanceof
的右側要求是一個構造函數,TypeScript將細化為:
- 此構造函數的
prototype
屬性的類型,如果它的類型不為any
的話 - 構造簽名所返回的類型的聯合
以此順序。
⒋可以為null的類型
TypeScript具有兩種特殊的類型, null
和 undefined
,它們分別具有值null和undefined. 我們在[基礎類型](./Basic Types.md)一節里已經做過簡要說明。 默認情況下,類型檢查器認為 null
與 undefined
可以賦值給任何類型。 null
與 undefined
是所有其它類型的一個有效值。 這也意味着,你阻止不了將它們賦值給其它類型,就算是你想要阻止這種情況也不行。 null
的發明者,Tony Hoare,稱它為 價值億萬美金的錯誤。
--strictNullChecks
標記可以解決此錯誤:當你聲明一個變量時,它不會自動地包含 null
或 undefined
。 你可以使用聯合類型明確的包含它們
let s = "foo"; s = null; // 錯誤, 'null'不能賦值給'string' let sn: string | null = "bar"; sn = null; // 可以 sn = undefined; // error, 'undefined'不能賦值給'string | null'
注意,按照JavaScript的語義,TypeScript會把 null
和 undefined
區別對待。 string | null
, string | undefined
和 string | undefined | null
是不同的類型。
⒌可選參數和可選屬性
使用了 --strictNullChecks
,可選參數會被自動地加上 | undefined
:
function f(x: number, y?: number) { return x + (y || 0); } f(1, 2); f(1); f(1, undefined); f(1, null); // error, 'null' is not assignable to 'number | undefined'
可選屬性也會有同樣的處理:
class C { a: number; b?: number; } let c = new C(); c.a = 12; c.a = undefined; // error, 'undefined' is not assignable to 'number' c.b = 13; c.b = undefined; // ok c.b = null; // error, 'null' is not assignable to 'number | undefined'
⒍類型保護和類型斷言
由於可以為null的類型是通過聯合類型實現,那么你需要使用類型保護來去除 null
。 幸運地是這與在JavaScript里寫的代碼一致:
function f(sn: string | null): string { if (sn == null) { return "default"; } else { return sn; } }
這里很明顯地去除了 null
,你也可以使用短路運算符:
function f(sn: string | null): string { return sn || "default"; }
如果編譯器不能夠去除 null
或 undefined
,你可以使用類型斷言手動去除。 語法是添加 !
后綴: identifier!
從 identifier
的類型里去除了 null
和 undefined
:
function broken(name: string | null): string { function postfix(epithet: string) { return name.charAt(0) + '. the ' + epithet; // error, 'name' is possibly null } name = name || "Bob"; return postfix("great"); } function fixed(name: string | null): string { function postfix(epithet: string) { return name!.charAt(0) + '. the ' + epithet; // ok } name = name || "Bob"; return postfix("great"); }
本例使用了嵌套函數,因為編譯器無法去除嵌套函數的null(除非是立即調用的函數表達式)。 因為它無法跟蹤所有對嵌套函數的調用,尤其是你將內層函數做為外層函數的返回值。 如果無法知道函數在哪里被調用,就無法知道調用時 name
的類型。
⒎類型別名
類型別名就是可以給一個類型起個新名字。類型別名有時和接口很像, 類型別名可以作用於原始值,聯合類型,元組以及其它任何你需要手寫的類型。
如果你學過C語言,可能還記得alias關鍵字,不過在TypeScript中,我們使用type關鍵字來描述類型變量。
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 Person<T> = {age : T};
也可以使用類型別名在屬性里引用自己,這看起來很像是遞歸。
type Person<T> = { name : T; mother : Person<T>; father : Person<T>; }
這使得類型編排非常復雜。當然,這種復雜性是為了描述的准確性,正如上面的例子,mother和father肯定也是person。這樣在代碼中看上去有點不可思議的操作,在現實世界中卻是非常真實合理的。
與交叉類型一起使用,我們可以創建出一些十分稀奇古怪的類型。
type LinkedList<T> = T & { next: LinkedList<T> }; interface Person { name: string; } var people: LinkedList<Person>; var s = people.name; var s = people.next.name; var s = people.next.next.name; var s = people.next.next.next.name;
然而,類型別名不能出現在聲明右側的任何地方。
type Yikes = Array<Yikes>; // error
接口 vs. 類型別名
像我們提到的,類型別名可以像接口一樣;然而,仍有一些細微差別。
其一,接口創建了一個新的名字,可以在其它任何地方使用。 類型別名並不創建新名字—比如,錯誤信息就不會使用別名。 在下面的示例代碼里,在編譯器中將鼠標懸停在 interfaced
上,顯示它返回的是 Interface
,但懸停在 aliased
上時,顯示的卻是對象字面量類型。
type Alias = { num: number } interface Interface { num: number; } declare function aliased(arg: Alias): Alias; declare function interfaced(arg: Interface): Interface;
另一個重要區別是類型別名不能被 extends
和 implements
(自己也不能 extends
和 implements
其它類型)。 因為 軟件中的對象應該對於擴展是開放的,但是對於修改是封閉的,你應該盡量去使用接口代替類型別名。
另一方面,如果你無法通過接口來描述一個類型並且需要使用聯合類型或元組類型,這時通常會使用類型別名。
⒏字符串字面量類型
我們先看一個簡單的字面量類型,比如下面這個字符串常量。
type Profession = "teacher";
字符串字面量類型允許你指定字符串必須的固定值。
在實際應用中,通常字符串字面量類型可以與聯合類型,類型保護和類型別名很好的配合。 而通過結合使用這些特性,達到類似枚舉類型的效果。
type Profession = "teacher" | "doctor" | "accountant"; function personCreator(Profession : Profession){ //省略函數內部的具體實現,這並不影響案例的運行 } personCreator("teacher"); personCreator("doctor"); personCreator("accountant"); personCreator("programmer"); //Error
你只能從三種允許的字符中選擇其一來做為參數傳遞,傳入其它值則會產生錯誤。【參見上面的聯合類型】
字符串字面量類型還可以用於區分函數重載:
function createElement(tagName: "img"): HTMLImageElement; function createElement(tagName: "input"): HTMLInputElement; // ... more overloads ... function createElement(tagName: string): Element { // ... code goes here ... }
⒐數字字面量類型
TypeScript還具有數字字面量類型,其用法和字符串字面量一致。
function rollDie(): 1 | 2 | 3 | 4 | 5 | 6 { // ... }
我們很少直接這樣使用,但它們可以用在縮小范圍調試bug的時候:
function foo(x: number) { if (x !== 1 || x !== 2) { // ~~~~~~~ // Operator '!==' cannot be applied to types '1' and '2'. } }
換句話說,當 x
與 2
進行比較的時候,它的值必須為 1
,這就意味着上面的比較檢查是非法的。
⒑枚舉成員類型
如我們在 枚舉一節里提到的,當每個枚舉成員都是用字面量初始化的時候枚舉成員是具有類型的。
在我們談及“單例類型”的時候,多數是指枚舉成員類型和數字/字符串字面量類型,盡管大多數用戶會互換使用“單例類型”和“字面量類型”。
可辨識聯合(Discriminated Unions)
你可以合並單例類型,聯合類型,類型保護和類型別名來創建一個叫做 可辨識聯合的高級模式,它也稱做 標簽聯合或 代數數據類型。 可辨識聯合在函數式編程很有用處。 一些語言會自動地為你辨識聯合;而TypeScript則基於已有的JavaScript模式。 它具有3個要素:
- 具有普通的單例類型屬性— 可辨識的特征。
- 一個類型別名包含了那些類型的聯合— 聯合。
- 此屬性上的類型保護。
interface Square { kind: "square"; size: number; } interface Rectangle { kind: "rectangle"; width: number; height: number; } interface Circle { kind: "circle"; radius: number; }
首先我們聲明了將要聯合的接口。 每個接口都有 kind
屬性但有不同的字符串字面量類型。 kind
屬性稱做 可辨識的特征或 標簽。 其它的屬性則特定於各個接口。 注意,目前各個接口間是沒有聯系的。 下面我們把它們聯合到一起:
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; } }
完整性檢查
當沒有涵蓋所有可辨識聯合的變化時,我們想讓編譯器可以通知我們。 比如,如果我們添加了 Triangle
到 Shape
,我們同時還需要更新 area
:
type Shape = Square | Rectangle | Circle | Triangle; 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; } // should error here - we didn't handle case "triangle" }
有兩種方式可以實現。 首先是啟用 --strictNullChecks
並且指定一個返回值類型:
function area(s: Shape): number { // error: returns number | undefined 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; } }
因為 switch
沒有包涵所有情況,所以TypeScript認為這個函數有時候會返回 undefined
。 如果你明確地指定了返回值類型為 number
,那么你會看到一個錯誤,因為實際上返回值的類型為 number | undefined
。 然而,這種方法存在些微妙之處且 --strictNullChecks
對舊代碼支持不好。
第二種方法使用 never
類型,編譯器用它來進行完整性檢查:
function assertNever(x: never): never { throw new Error("Unexpected object: " + x); } 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; default: return assertNever(s); // error here if there are missing cases } }
這里, assertNever
檢查 s
是否為 never
類型—即為除去所有可能情況后剩下的類型。 如果你忘記了某個case,那么 s
將具有一個真實的類型並且你會得到一個錯誤。 這種方式需要你定義一個額外的函數,但是在你忘記某個case的時候也更加明顯。
多態的 this
類型
多態的 this
類型表示的是某個包含類或接口的 子類型。 這被稱做 F-bounded多態性。 它能很容易的表現連貫接口間的繼承,比如。 在計算器的例子里,在每個操作之后都返回 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
。
索引類型(Index types)
使用索引類型,編譯器就能夠檢查使用了動態屬性名的代碼。例如,一個常見的JavaScript模式是從對象中選取屬性的子集。
function pluck(o, names) { return names.map(n => o[n]); }
在TypeScript里通過 索引類型查詢和 索引訪問操作符使用此函數:
function pluck<T, K extends keyof T>(o: T, names: K[]): T[K][] { return names.map(n => o[n]); } interface Person { name: string; age: number; } let person: Person = { name: 'Jarid', age: 35 }; let strings: string[] = pluck(person, ['name']); // ok, string[]
編譯器會檢查傳入的值是否是 Person
的一個屬性。 本例還引入了幾個新的類型操作符。 首先是 keyof T
, 索引類型查詢操作符。 對於任何類型 T
, keyof T
的結果為 T
上已知的公共屬性名的聯合。 例如:
let personProps: keyof Person; // 'name' | 'age'
keyof Person
是完全可以與 'name' | 'age'
互相替換的。 不同的是如果你為Person添加了新的屬性 ,例如 address: string
,那么 keyof Person
會自動變為 'name' | 'age' | 'address'
。 你可以在像 pluck
函數這類上下文里使用 keyof
,因為在使用之前你並不清楚可能出現的屬性名。 但編譯器會檢查你是否傳入了正確的屬性名給 pluck
:
pluck(person, ['age', 'unknown']); // error, 'unknown' is not in 'name' | 'age'
第二個操作符是 T[K]
, 索引訪問操作符。 在這里,類型語法反映了表達式語法。 這意味着 person['name']
具有類型 Person['name']
— 在我們的例子里則為 string
類型。 然而,就像索引類型查詢一樣,你可以在普通的上下文里使用 T[K]
,這正是它的強大所在。 你只要確保類型變量 K extends keyof T
就可以了。 例如下面 getProperty
函數的例子:
function getProperty<T, K extends keyof T>(o: T, name: K): T[K] { return o[name]; // o[name] is of type T[K] }
getProperty
里的 o: T
和 name: K
,意味着 o[name]: T[K]
。 當你返回 T[K]
的結果,編譯器會實例化鍵的真實類型,因此 getProperty
的返回值類型會隨着你需要的屬性改變。
let name: string = getProperty(person, 'name'); let age: number = getProperty(person, 'age'); let unknown = getProperty(person, 'unknown'); // error, 'unknown' is not in 'name' | 'age'
索引類型和字符串索引簽名
keyof
和 T[K]
與字符串索引簽名進行交互。 如果你有一個帶有字符串索引簽名的類型,那么 keyof T
會是 string
。 並且 T[string]
為索引簽名的類型:
interface Map<T> { [key: string]: T; } let keys: keyof Map<number>; // string let value: Map<number>['foo']; // number
讓我們解釋下上面一開始代碼的意義。首先看泛型,這里有T和K兩種類型。根據類型推斷,第一個參數o就是person,類型會被推斷為Person,而第二個數組參數的類型推斷,我們可以從右往左進行閱讀,keyof關鍵字可以獲取T(此處為Person)的所有屬性名,即['name','age'],泛型K通過extends關鍵字繼承了T(此處為Person)的所有屬性名,即['name','age']。
依托於keyof關鍵字完成了類型索引。
我們再來看返回值,返回值的類型是T[K][],閱讀起來有些困難,它實際上表述的意思是,變量T取屬性K的值的數組,其中T[K]就是索引訪問操作符。
這樣強大的功能保證了代碼的動態性和准確性,也讓代碼提示變得更加豐富了。
映射類型
一種常見的場景是將一個已知類型的每個屬性都變為可選的,這樣在實例化該類型時就不必為每個類型都賦值了。
interface Person { name?: string; age?: number; }
或者是我們想要一個該類型的只讀版本【即該類型的屬性值都是只讀不可修改的】
interface Person {
readonly name: string;
readonly age: number;
}
這在JavaScript里會經常用到,而TypeScript提供了從舊類型中創建新類型的一種方式 — 映射類型。 在映射類型里,新類型以相同的形式去轉換舊類型里每個屬性。 例如,你可以令每個屬性成為只讀類型或可選類型。 下面是一些例子:
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>;
下面來看看最簡單的映射類型和它的組成部分:
type Keys = 'option1' | 'option2'; type Flags = { [K in Keys]: boolean };
它的語法與索引簽名的語法類型,內部使用了 for .. in
。 具有三個部分:
- 類型變量
K
,它會依次綁定到每個屬性。 - 字符串字面量聯合的
Keys
,它包含了要迭代的屬性名的集合。 - 屬性的結果類型。
在個簡單的例子里, Keys
是硬編碼的的屬性名列表並且屬性類型永遠是 boolean
,因此這個映射類型等同於:
type Flags = { option1: boolean; option2: boolean; }
在真正的應用里,可能不同於上面的 Readonly
或 Partial
。 它們會基於一些已存在的類型,且按照一定的方式轉換字段。 這就是 keyof
和索引訪問類型要做的事情:
type NullablePerson = { [P in keyof Person]: Person[P] | null } type PartialPerson = { [P in keyof Person]?: Person[P] }
但它更有用的地方是可以有一些通用版本。
type Nullable<T> = { [P in keyof T]: T[P] | null } type Partial<T> = { [P in keyof T]?: T[P] }
在這些例子里,屬性列表是 keyof T
且結果類型是 T[P]
的變體。 這是使用通用映射類型的一個好模版。 因為這類轉換是 同態的,映射只作用於 T
的屬性而沒有其它的。 編譯器知道在添加任何新屬性之前可以拷貝所有存在的屬性修飾符。 例如,假設 Person.name
是只讀的,那么 Partial<Person>.name
也將是只讀的且為可選的。
下面是另一個例子, T[P]
被包裝在 Proxy<T>
類里:
type Proxy<T> = { get(): T; set(value: T): void; } type Proxify<T> = { [P in keyof T]: Proxy<T[P]>; } function proxify<T>(o: T): Proxify<T> { // ... wrap proxies ... } let proxyProps = proxify(props);
注意 Readonly<T>
和 Partial<T>
用處不小,因此它們與 Pick
和 Record
一同被包含進了TypeScript的標准庫里:
type Pick<T, K extends keyof T> = { [P in K]: T[P]; } type Record<K extends string, T> = { [P in K]: T; }
Readonly
, Partial
和 Pick
是同態的,但 Record
不是。 因為 Record
並不需要輸入類型來拷貝屬性,所以它不屬於同態:
type ThreeStringProps = Record<'prop1' | 'prop2' | 'prop3', string>
非同態類型本質上會創建新的屬性,因此它們不會從它處拷貝屬性修飾符。
TypeScript中內置了Readonly和Partial,所以不需要手動聲明實現。
內置的類型還有Required、Pick、Record、Exclude、Extract、NonNullable;它們的實現都在typescript/lib/lib.es5.d.ts中。
由映射類型進行推斷
現在你了解了如何包裝一個類型的屬性,那么接下來就是如何拆包。 其實這也非常容易:
function unproxify<T>(t: Proxify<T>): T { let result = {} as T; for (const k in t) { result[k] = t[k].get(); } return result; } let originalProps = unproxify(proxyProps);
注意這個拆包推斷只適用於同態的映射類型。 如果映射類型不是同態的,那么需要給拆包函數一個明確的類型參數。
預定義的有條件類型
TypeScript 2.8在lib.d.ts
里增加了一些預定義的有條件類型:
Exclude<T, U>
-- 從T
中剔除可以賦值給U
的類型。Extract<T, U>
-- 提取T
中可以賦值給U
的類型。NonNullable<T>
-- 從T
中剔除null
和undefined
。ReturnType<T>
-- 獲取函數返回值類型。InstanceType<T>
-- 獲取構造函數類型的實例類型。
示例
type T00 = Exclude<"a" | "b" | "c" | "d", "a" | "c" | "f">; // "b" | "d" type T01 = Extract<"a" | "b" | "c" | "d", "a" | "c" | "f">; // "a" | "c" type T02 = Exclude<string | number | (() => void), Function>; // string | number type T03 = Extract<string | number | (() => void), Function>; // () => void type T04 = NonNullable<string | number | undefined>; // string | number type T05 = NonNullable<(() => string) | string[] | null | undefined>; // (() => string) | string[] function f1(s: string) { return { a: 1, b: s }; } class C { x = 0; y = 0; } type T10 = ReturnType<() => string>; // string type T11 = ReturnType<(s: string) => void>; // void type T12 = ReturnType<(<T>() => T)>; // {} type T13 = ReturnType<(<T extends U, U extends number[]>() => T)>; // number[] type T14 = ReturnType<typeof f1>; // { a: number, b: string } type T15 = ReturnType<any>; // any type T16 = ReturnType<never>; // any type T17 = ReturnType<string>; // Error type T18 = ReturnType<Function>; // Error type T20 = InstanceType<typeof C>; // C type T21 = InstanceType<any>; // any type T22 = InstanceType<never>; // any type T23 = InstanceType<string>; // Error type T24 = InstanceType<Function>; // Error
注意:Exclude
類型是建議的Diff
類型的一種實現。我們使用Exclude
這個名字是為了避免破壞已經定義了Diff
的代碼,並且我們感覺這個名字能更好地表達類型的語義。我們沒有增加Omit<T, K>
類型,因為它可以很容易的用Pick<T, Exclude<keyof T, K>>
來表示。