條件型別 (Conditional Types)
TypeScript 2.8 引入了條件型別,它增加了表達非均勻型別對映的能力。條件型別根據型別關係測試所表達的條件,在兩種可能的型別中選擇一種。
tsT extends U ? X : Y
上述型別表示:當 T 可以賦值給 U 時,型別為 X,否則型別為 Y。
條件型別 T extends U ? X : Y 要麼被解析為 X 或 Y,要麼被延遲,因為該條件依賴於一個或多個型別變數。解析還是延遲由以下規則決定:
- 首先,給定
T和U的例項化型別T'和U'(其中型別引數的所有出現都被替換為any),如果T'不能賦值給U',則條件型別解析為Y。直觀地講,如果T的最寬鬆例項化不能賦值給U的最寬鬆例項化,我們知道任何例項化都不會成功,因此可以直接解析為Y。 - 接下來,對於
U中由infer(稍後詳述)宣告引入的每個型別變數,透過從T到U的推斷(使用與泛型函式型別推斷相同的演算法)來收集一組候選型別。對於給定的infer型別變數V,如果從協變位置推斷出任何候選型別,則為V推斷出的型別是這些候選型別的聯合。否則,如果從逆變位置推斷出任何候選型別,則為V推斷出的型別是這些候選型別的交集。否則,為V推斷出的型別是never。 - 然後,給定
T的例項化型別T''(其中所有infer型別變數被替換為上一步中推斷出的型別),如果T''明確可賦值於U,則條件型別解析為X。“明確可賦值”關係與常規的可賦值關係相同,只是不考慮型別變數約束。直觀地講,當一個型別明確可賦值於另一個型別時,我們知道它對於這些型別的所有例項化都將是可賦值的。 - 否則,該條件依賴於一個或多個型別變數,條件型別被延遲處理。
示例
tstype TypeName<T> = T extends string? "string": T extends number? "number": T extends boolean? "boolean": T extends undefined? "undefined": T extends Function? "function": "object";type T0 = TypeName<string>; // "string"type T1 = TypeName<"a">; // "string"type T2 = TypeName<true>; // "boolean"type T3 = TypeName<() => void>; // "function"type T4 = TypeName<string[]>; // "object"
分散式條件型別 (Distributive conditional types)
檢查型別為裸型別引數的條件型別被稱為分散式條件型別。分散式條件型別在例項化過程中會自動分發到聯合型別上。例如,使用型別引數 A | B | C 例項化 T extends U ? X : Y(針對 T)時,它被解析為 (A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)。
示例
tstype T10 = TypeName<string | (() => void)>; // "string" | "function"type T12 = TypeName<string | string[] | undefined>; // "string" | "object" | "undefined"type T11 = TypeName<string[] | number[]>; // "object"
在例項化分散式條件型別 T extends U ? X : Y 時,條件型別內對 T 的引用會被解析為聯合型別的各個成員(即 T 指代的是條件型別分發到聯合型別之後的各個成員)。此外,X 中對 T 的引用具有額外的型別引數約束 U(即 T 在 X 中被視為可賦值於 U)。
示例
tstype BoxedValue<T> = { value: T };type BoxedArray<T> = { array: T[] };type Boxed<T> = T extends any[] ? BoxedArray<T[number]> : BoxedValue<T>;type T20 = Boxed<string>; // BoxedValue<string>;type T21 = Boxed<number[]>; // BoxedArray<number>;type T22 = Boxed<string | number[]>; // BoxedValue<string> | BoxedArray<number>;
請注意,T 在 Boxed<T> 的真分支中具有額外的約束 any[],因此可以將陣列的元素型別引用為 T[number]。還要注意在最後一個示例中條件型別是如何分發到聯合型別上的。
條件型別的分散式特性可以方便地用於過濾聯合型別。
tstype Diff<T, U> = T extends U ? never : T; // Remove types from T that are assignable to Utype Filter<T, U> = T extends U ? T : never; // Remove types from T that are not assignable to Utype T30 = Diff<"a" | "b" | "c" | "d", "a" | "c" | "f">; // "b" | "d"type T31 = Filter<"a" | "b" | "c" | "d", "a" | "c" | "f">; // "a" | "c"type T32 = Diff<string | number | (() => void), Function>; // string | numbertype T33 = Filter<string | number | (() => void), Function>; // () => voidtype NonNullable<T> = Diff<T, null | undefined>; // Remove null and undefined from Ttype T34 = NonNullable<string | number | undefined>; // string | numbertype T35 = NonNullable<string | string[] | null | undefined>; // string | string[]function f1<T>(x: T, y: NonNullable<T>) {x = y; // Oky = x; // Error}function f2<T extends string | undefined>(x: T, y: NonNullable<T>) {x = y; // Oky = x; // Errorlet s1: string = x; // Errorlet s2: string = y; // Ok}
條件型別與對映型別結合使用時特別有用。
tstype FunctionPropertyNames<T> = {[K in keyof T]: T[K] extends Function ? K : never;}[keyof T];type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>>;type NonFunctionPropertyNames<T> = {[K in keyof T]: T[K] extends Function ? never : K;}[keyof T];type NonFunctionProperties<T> = Pick<T, NonFunctionPropertyNames<T>>;interface Part {id: number;name: string;subparts: Part[];updatePart(newName: string): void;}type T40 = FunctionPropertyNames<Part>; // "updatePart"type T41 = NonFunctionPropertyNames<Part>; // "id" | "name" | "subparts"type T42 = FunctionProperties<Part>; // { updatePart(newName: string): void }type T43 = NonFunctionProperties<Part>; // { id: number, name: string, subparts: Part[] }
與聯合型別和交集型別類似,條件型別不允許遞迴地引用自身。例如,以下情況會報錯。
示例
tstype ElementType<T> = T extends any[] ? ElementType<T[number]> : T; // Error
條件型別中的型別推斷 (Type inference in conditional types)
在條件型別的 extends 子句中,現在可以使用 infer 宣告來引入一個待推斷的型別變數。這些被推斷出的型別變數可以在條件型別的真分支中被引用。同一個型別變數可以有多個 infer 位置。
例如,以下程式碼提取函式型別的返回型別:
tstype ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
條件型別可以巢狀,形成按順序評估的一系列模式匹配。
tstype Unpacked<T> = T extends (infer U)[]? U: T extends (...args: any[]) => infer U? U: T extends Promise<infer U>? U: T;type T0 = Unpacked<string>; // stringtype T1 = Unpacked<string[]>; // stringtype T2 = Unpacked<() => string>; // stringtype T3 = Unpacked<Promise<string>>; // stringtype T4 = Unpacked<Promise<string>[]>; // Promise<string>type T5 = Unpacked<Unpacked<Promise<string>[]>>; // string
以下示例演示了在協變位置中,同一個型別變數的多個候選型別是如何導致推斷出聯合型別的:
tstype Foo<T> = T extends { a: infer U; b: infer U } ? U : never;type T10 = Foo<{ a: string; b: string }>; // stringtype T11 = Foo<{ a: string; b: number }>; // string | number
同樣,在逆變位置中,同一個型別變數的多個候選型別會導致推斷出交集型別。
tstype Bar<T> = T extends { a: (x: infer U) => void; b: (x: infer U) => void }? U: never;type T20 = Bar<{ a: (x: string) => void; b: (x: string) => void }>; // stringtype T21 = Bar<{ a: (x: string) => void; b: (x: number) => void }>; // string & number
當從具有多個呼叫簽名的型別(例如過載函式的型別)進行推斷時,會從最後一個簽名(通常是最寬鬆的兜底情況)進行推斷。無法根據引數型別列表執行過載解析。
tsdeclare function foo(x: string): number;declare function foo(x: number): string;declare function foo(x: string | number): string | number;type T30 = ReturnType<typeof foo>; // string | number
在常規型別引數的約束子句中不能使用 infer 宣告。
tstype ReturnType<T extends (...args: any[]) => infer R> = R; // Error, not supported
然而,透過清除約束中的型別變數並指定條件型別,可以達到類似的效果。
tstype AnyFunction = (...args: any[]) => any;type ReturnType<T extends AnyFunction> = T extends (...args: any[]) => infer R? R: any;
預定義的條件型別 (Predefined conditional types)
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>— 獲取建構函式型別的例項型別。
示例
tstype 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 | numbertype T03 = Extract<string | number | (() => void), Function>; // () => voidtype T04 = NonNullable<string | number | undefined>; // string | numbertype 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>; // stringtype T11 = ReturnType<(s: string) => void>; // voidtype 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>; // anytype T16 = ReturnType<never>; // anytype T17 = ReturnType<string>; // Errortype T18 = ReturnType<Function>; // Errortype T20 = InstanceType<typeof C>; // Ctype T21 = InstanceType<any>; // anytype T22 = InstanceType<never>; // anytype T23 = InstanceType<string>; // Errortype T24 = InstanceType<Function>; // Error
注意:
Exclude型別是對此處建議的Diff型別的正確實現。我們使用Exclude這個名稱是為了避免破壞現有定義了Diff的程式碼,此外我們認為這個名稱更能傳達該型別的語義。我們沒有包含Omit<T, K>型別,因為它可以用Pick<T, Exclude<keyof T, K>>簡單地編寫。
改進對對映型別修飾符的控制 (Improved control over mapped type modifiers)
對映型別支援向對映屬性新增 readonly 或 ? 修飾符,但此前不支援移除修飾符。這在同態對映型別中很重要,它們預設會保留底層型別的修飾符。
TypeScript 2.8 允許對映型別新增或移除特定修飾符。具體來說,對映型別中的 readonly 或 ? 屬性修飾符現在可以新增 + 或 - 字首,以表明該修飾符應被新增或移除。
示例
tstype MutableRequired<T> = { -readonly [P in keyof T]-?: T[P] }; // Remove readonly and ?type ReadonlyPartial<T> = { +readonly [P in keyof T]+?: T[P] }; // Add readonly and ?
沒有 + 或 - 字首的修飾符等同於帶有 + 字首的修飾符。因此,上述 ReadonlyPartial<T> 型別對應於:
tstype ReadonlyPartial<T> = { readonly [P in keyof T]?: T[P] }; // Add readonly and ?
利用此能力,lib.d.ts 現在有了新的 Required<T> 型別。該型別會剝離 T 中所有屬性的 ? 修飾符,從而使所有屬性變為必選。
示例
tstype Required<T> = { [P in keyof T]-?: T[P] };
請注意,在 strictNullChecks 模式下,當同態對映型別從底層型別的屬性中移除 ? 修飾符時,它同時也會從該屬性的型別中移除 undefined。
示例
tstype Foo = { a?: string }; // Same as { a?: string | undefined }type Bar = Required<Foo>; // Same as { a: string }
改進 keyof 對交集型別的支援 (Improved keyof with intersection types)
使用 TypeScript 2.8,應用於交集型別的 keyof 被轉換為應用於每個交整合員的 keyof 的聯合。換句話說,形式為 keyof (A & B) 的型別被轉換為 keyof A | keyof B。此更改解決了 keyof 表示式推斷中的不一致問題。
示例
tstype A = { a: string };type B = { b: string };type T1 = keyof (A & B); // "a" | "b"type T2<T> = keyof (T & B); // keyof T | "b"type T3<U> = keyof (A & U); // "a" | keyof Utype T4<T, U> = keyof (T & U); // keyof T | keyof Utype T5 = T2<A>; // "a" | "b"type T6 = T3<B>; // "a" | "b"type T7 = T4<A, B>; // "a" | "b"
更好地處理 .js 檔案中的名稱空間模式 (Better handling for namespace patterns in .js files)
TypeScript 2.8 增加了對 .js 檔案中更多名稱空間模式的理解支援。頂層空物件字面量宣告,就像函式和類一樣,現在被識別為 JavaScript 中的名稱空間宣告。
jsvar ns = {}; // recognized as a declaration for a namespace `ns`ns.constant = 1; // recognized as a declaration for var `constant`
頂層的賦值應該以同樣的方式表現;換句話說,不需要 var 或 const 宣告。
jsapp = {}; // does NOT need to be `var app = {}`app.C = class {};app.f = function() {};app.prop = 1;
作為名稱空間宣告的 IIFE (IIFEs as namespace declarations)
返回函式、類或空物件字面量的 IIFE 也被識別為名稱空間。
jsvar C = (function() {function C(n) {this.p = n;}return C;})();C.staticProperty = 1;
預設賦值宣告 (Defaulted declarations)
“預設賦值宣告”允許在初始化程式中引用邏輯或運算子左側的宣告名稱。
jsmy = window.my || {};my.app = my.app || {};
原型賦值 (Prototype assignment)
您可以直接將物件字面量賦值給 prototype 屬性。單獨的原型賦值也仍然有效。
tsvar C = function(p) {this.p = p;};C.prototype = {m() {console.log(this.p);}};C.prototype.q = function(r) {return this.p === r;};
巢狀和合並宣告 (Nested and merged declarations)
現在巢狀可以支援任何深度,並且可以跨檔案正確合併。此前這兩點都無法實現。
jsvar app = window.app || {};app.C = class {};
檔案級 JSX 工廠 (Per-file JSX factories)
TypeScript 2.8 增加了對使用 @jsx dom 指令配置各檔案 JSX 工廠名稱的支援。JSX 工廠可以透過 jsxFactory(預設為 React.createElement)為整個編譯進行配置。在 TypeScript 2.8 中,您可以透過在檔案開頭添加註釋來覆蓋此項配置。
示例
ts/** @jsx dom */import { dom } from "./renderer";<h></h>;
生成:
jsvar renderer_1 = require("./renderer");renderer_1.dom("h", null);
區域性作用域 JSX 名稱空間 (Locally scoped JSX namespaces)
JSX 型別檢查由 JSX 名稱空間中的定義驅動,例如用於 JSX 元素型別的 JSX.Element,以及用於內建元素的 JSX.IntrinsicElements。在 TypeScript 2.8 之前,JSX 名稱空間被期望位於全域性名稱空間中,因此專案內只能定義一個。從 TypeScript 2.8 開始,JSX 名稱空間將在 jsxNamespace(例如 React)下查詢,從而允許在一次編譯中使用多個 JSX 工廠。為了向後相容,如果工廠函式上未定義 JSX 名稱空間,則會使用全域性 JSX 名稱空間作為兜底。結合檔案級的 @jsx 指令,每個檔案都可以擁有不同的 JSX 工廠。
新增 --emitDeclarationOnly
emitDeclarationOnly 允許只生成宣告檔案;使用此標誌將跳過 .js/.jsx 輸出的生成。當 .js 輸出的生成由 Babel 等其他轉譯器處理時,此標誌非常有用。