TypeScript 2.8

條件型別 (Conditional Types)

TypeScript 2.8 引入了條件型別,它增加了表達非均勻型別對映的能力。條件型別根據型別關係測試所表達的條件,在兩種可能的型別中選擇一種。

ts
T extends U ? X : Y

上述型別表示:當 T 可以賦值給 U 時,型別為 X,否則型別為 Y

條件型別 T extends U ? X : Y 要麼被解析XY,要麼被延遲,因為該條件依賴於一個或多個型別變數。解析還是延遲由以下規則決定:

  • 首先,給定 TU 的例項化型別 T'U'(其中型別引數的所有出現都被替換為 any),如果 T' 不能賦值給 U',則條件型別解析為 Y。直觀地講,如果 T 的最寬鬆例項化不能賦值給 U 的最寬鬆例項化,我們知道任何例項化都不會成功,因此可以直接解析為 Y
  • 接下來,對於 U 中由 infer(稍後詳述)宣告引入的每個型別變數,透過從 TU 的推斷(使用與泛型函式型別推斷相同的演算法)來收集一組候選型別。對於給定的 infer 型別變數 V,如果從協變位置推斷出任何候選型別,則為 V 推斷出的型別是這些候選型別的聯合。否則,如果從逆變位置推斷出任何候選型別,則為 V 推斷出的型別是這些候選型別的交集。否則,為 V 推斷出的型別是 never
  • 然後,給定 T 的例項化型別 T''(其中所有 infer 型別變數被替換為上一步中推斷出的型別),如果 T'' 明確可賦值U,則條件型別解析為 X。“明確可賦值”關係與常規的可賦值關係相同,只是不考慮型別變數約束。直觀地講,當一個型別明確可賦值於另一個型別時,我們知道它對於這些型別的所有例項化都將是可賦值的。
  • 否則,該條件依賴於一個或多個型別變數,條件型別被延遲處理。
示例
ts
type 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)

示例
ts
type 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(即 TX 中被視為可賦值於 U)。

示例
ts
type 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>;

請注意,TBoxed<T> 的真分支中具有額外的約束 any[],因此可以將陣列的元素型別引用為 T[number]。還要注意在最後一個示例中條件型別是如何分發到聯合型別上的。

條件型別的分散式特性可以方便地用於過濾聯合型別。

ts
type Diff<T, U> = T extends U ? never : T; // Remove types from T that are assignable to U
type Filter<T, U> = T extends U ? T : never; // Remove types from T that are not assignable to U
type 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 | number
type T33 = Filter<string | number | (() => void), Function>; // () => void
type NonNullable<T> = Diff<T, null | undefined>; // Remove null and undefined from T
type T34 = NonNullable<string | number | undefined>; // string | number
type T35 = NonNullable<string | string[] | null | undefined>; // string | string[]
function f1<T>(x: T, y: NonNullable<T>) {
x = y; // Ok
y = x; // Error
}
function f2<T extends string | undefined>(x: T, y: NonNullable<T>) {
x = y; // Ok
y = x; // Error
let s1: string = x; // Error
let s2: string = y; // Ok
}

條件型別與對映型別結合使用時特別有用。

ts
type 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[] }

與聯合型別和交集型別類似,條件型別不允許遞迴地引用自身。例如,以下情況會報錯。

示例
ts
type ElementType<T> = T extends any[] ? ElementType<T[number]> : T; // Error

條件型別中的型別推斷 (Type inference in conditional types)

在條件型別的 extends 子句中,現在可以使用 infer 宣告來引入一個待推斷的型別變數。這些被推斷出的型別變數可以在條件型別的真分支中被引用。同一個型別變數可以有多個 infer 位置。

例如,以下程式碼提取函式型別的返回型別:

ts
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;

條件型別可以巢狀,形成按順序評估的一系列模式匹配。

ts
type 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>; // string
type T1 = Unpacked<string[]>; // string
type T2 = Unpacked<() => string>; // string
type T3 = Unpacked<Promise<string>>; // string
type T4 = Unpacked<Promise<string>[]>; // Promise<string>
type T5 = Unpacked<Unpacked<Promise<string>[]>>; // string

以下示例演示了在協變位置中,同一個型別變數的多個候選型別是如何導致推斷出聯合型別的:

ts
type Foo<T> = T extends { a: infer U; b: infer U } ? U : never;
type T10 = Foo<{ a: string; b: string }>; // string
type T11 = Foo<{ a: string; b: number }>; // string | number

同樣,在逆變位置中,同一個型別變數的多個候選型別會導致推斷出交集型別。

ts
type 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 }>; // string
type T21 = Bar<{ a: (x: string) => void; b: (x: number) => void }>; // string & number

當從具有多個呼叫簽名的型別(例如過載函式的型別)進行推斷時,會從最後一個簽名(通常是最寬鬆的兜底情況)進行推斷。無法根據引數型別列表執行過載解析。

ts
declare 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 宣告。

ts
type ReturnType<T extends (...args: any[]) => infer R> = R; // Error, not supported

然而,透過清除約束中的型別變數並指定條件型別,可以達到類似的效果。

ts
type 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 中排除 nullundefined
  • ReturnType<T> — 獲取函式型別的返回型別。
  • InstanceType<T> — 獲取建構函式型別的例項型別。
示例
ts
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>> 簡單地編寫。

改進對對映型別修飾符的控制 (Improved control over mapped type modifiers)

對映型別支援向對映屬性新增 readonly? 修飾符,但此前不支援移除修飾符。這在同態對映型別中很重要,它們預設會保留底層型別的修飾符。

TypeScript 2.8 允許對映型別新增或移除特定修飾符。具體來說,對映型別中的 readonly? 屬性修飾符現在可以新增 +- 字首,以表明該修飾符應被新增或移除。

示例

ts
type 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> 型別對應於:

ts
type ReadonlyPartial<T> = { readonly [P in keyof T]?: T[P] }; // Add readonly and ?

利用此能力,lib.d.ts 現在有了新的 Required<T> 型別。該型別會剝離 T 中所有屬性的 ? 修飾符,從而使所有屬性變為必選。

示例
ts
type Required<T> = { [P in keyof T]-?: T[P] };

請注意,在 strictNullChecks 模式下,當同態對映型別從底層型別的屬性中移除 ? 修飾符時,它同時也會從該屬性的型別中移除 undefined

示例
ts
type 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 表示式推斷中的不一致問題。

示例
ts
type 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 U
type T4<T, U> = keyof (T & U); // keyof T | keyof U
type 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 中的名稱空間宣告。

js
var ns = {}; // recognized as a declaration for a namespace `ns`
ns.constant = 1; // recognized as a declaration for var `constant`

頂層的賦值應該以同樣的方式表現;換句話說,不需要 varconst 宣告。

js
app = {}; // does NOT need to be `var app = {}`
app.C = class {};
app.f = function() {};
app.prop = 1;

作為名稱空間宣告的 IIFE (IIFEs as namespace declarations)

返回函式、類或空物件字面量的 IIFE 也被識別為名稱空間。

js
var C = (function() {
function C(n) {
this.p = n;
}
return C;
})();
C.staticProperty = 1;

預設賦值宣告 (Defaulted declarations)

“預設賦值宣告”允許在初始化程式中引用邏輯或運算子左側的宣告名稱。

js
my = window.my || {};
my.app = my.app || {};

原型賦值 (Prototype assignment)

您可以直接將物件字面量賦值給 prototype 屬性。單獨的原型賦值也仍然有效。

ts
var 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)

現在巢狀可以支援任何深度,並且可以跨檔案正確合併。此前這兩點都無法實現。

js
var 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>;

生成:

js
var 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 等其他轉譯器處理時,此標誌非常有用。

TypeScript 文件是一個開源專案。歡迎提交 Pull Request 來幫助我們改進這些頁面 ❤

此頁面的貢獻者
MHMohamed Hegazy (55)
OTOrta Therox (12)
EIEugene Ilyin (1)
DKDongho Kim (1)
JBJack Bates (1)
6+

最後更新:2026 年 3 月 27 日