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 日