TypeScript 2.8

条件类型

🌐 Conditional Types

TypeScript 2.8 引入了 条件类型,它增加了表达非统一类型映射的能力。条件类型根据类型关系测试表达的条件选择两种可能类型中的一种:

🌐 TypeScript 2.8 introduces conditional types which add the ability to express non-uniform type mappings. A conditional type selects one of two possible types based on a condition expressed as a type relationship test:

ts
T extends U ? X : Y

上面的类型意思是,当 T 可以赋值给 U 时,类型为 X,否则类型为 Y

🌐 The type above means when T is assignable to U the type is X, otherwise the type is Y.

条件类型 T extends U ? X : Y 要么被 解析XY,要么被 延迟,因为条件依赖于一个或多个类型变量。是否解析或延迟由以下方式决定:

🌐 A conditional type T extends U ? X : Y is either resolved to X or Y, or deferred because the condition depends on one or more type variables. Whether to resolve or defer is determined as follows:

  • 首先,给定类型 T'U',它们是 TU 的实例化,其中所有类型参数都被替换为 any,如果 T' 不能赋值给 U',则条件类型将解析为 Y。直观地说,如果 T 的最宽松实例化不能赋值给 U 的最宽松实例化,我们就知道没有实例化能够赋值,因此我们可以直接解析为 Y
  • 接下来,对于 U 中由 infer(稍后介绍)声明引入的每个类型变量,通过从 T 推断到 U 来收集一组候选类型(使用与泛型函数类型推断相同的推断算法)。对于给定的 infer 类型变量 V,如果从协变位置推断出任何候选类型,则为 V 推断的类型是这些候选类型的并集。否则,如果从逆变位置推断出任何候选类型,则为 V 推断的类型是这些候选类型的交集。否则,为 V 推断的类型为 never
  • 然后,给定一个类型 T'',它是 T 的实例化,其中所有 infer 类型变量都被前一步推断出的类型替换,如果 T'' 绝对可赋值U,则条件类型解析为 X。绝对可赋值关系与常规可赋值关系相同,只是类型变量约束不被考虑。直观上,当一个类型绝对可赋值给另一个类型时,我们知道对于这些类型的 所有实例化 都是可赋值的。
  • 否则,条件依赖于一个或多个类型变量,并且条件类型将被延迟。
示例

🌐 Example

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 实例化 T extends U ? X : Y 的结果被解析为 (A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)

🌐 Conditional types in which the checked type is a naked type parameter are called distributive conditional types. Distributive conditional types are automatically distributed over union types during instantiation. For example, an instantiation of T extends U ? X : Y with the type argument A | B | C for T is resolved as (A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y).

示例

🌐 Example

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(即在 X 中,T 被视为可以赋值给 U)。

🌐 In instantiations of a distributive conditional type T extends U ? X : Y, references to T within the conditional type are resolved to individual constituents of the union type (i.e. T refers to the individual constituents after the conditional type is distributed over the union type). Furthermore, references to T within X have an additional type parameter constraint U (i.e. T is considered assignable to U within X).

示例

🌐 Example

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]。另外,请注意最后一个例子中条件类型如何分配到联合类型上。

🌐 Notice that T has the additional constraint any[] within the true branch of Boxed<T> and it is therefore possible to refer to the element type of the array as T[number]. Also, notice how the conditional type is distributed over the union type in the last example.

条件类型的分配属性可以方便地用于筛选联合类型:

🌐 The distributive property of conditional types can conveniently be used to filter union types:

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
}

条件类型与映射类型结合使用时尤其有用:

🌐 Conditional types are particularly useful when combined with mapped types:

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

类似于联合类型和交叉类型,条件类型不允许递归地引用自身。例如,以下是一个错误示例。

🌐 Similar to union and intersection types, conditional types are not permitted to reference themselves recursively. For example the following is an error.

示例

🌐 Example

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

条件类型中的类型推断

🌐 Type inference in conditional types

在条件类型的 extends 子句中,现在可以有 infer 声明来引入需要推断的类型变量。 这些被推断的类型变量可以在条件类型的 true 分支中引用。 对于同一个类型变量,可以有多个 infer 位置。

🌐 Within the extends clause of a conditional type, it is now possible to have infer declarations that introduce a type variable to be inferred. Such inferred type variables may be referenced in the true branch of the conditional type. It is possible to have multiple infer locations for the same type variable.

例如,以下代码提取函数类型的返回类型:

🌐 For example, the following extracts the return type of a function type:

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

条件类型可以嵌套,形成按顺序执行的模式匹配序列:

🌐 Conditional types can be nested to form a sequence of pattern matches that are evaluated in order:

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

以下示例演示了如何在共变位置上为同一类型变量指定多个候选值,从而推断出联合类型:

🌐 The following example demonstrates how multiple candidates for the same type variable in co-variant positions causes a union type to be inferred:

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

同样,如果同一类型变量在相反变量位置有多个候选,则会推断为交叉类型:

🌐 Likewise, multiple candidates for the same type variable in contra-variant positions causes an intersection type to be inferred:

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

当从具有多个调用签名的类型(例如重载函数的类型)进行推断时,推断是基于最后一个签名进行的(大概这是最宽松的通用情况)。 无法根据参数类型列表进行重载解析。

🌐 When inferring from a type with multiple call signatures (such as the type of an overloaded function), inferences are made from the last signature (which, presumably, is the most permissive catch-all case). It is not possible to perform overload resolution based on a list of argument types.

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 声明:

🌐 It is not possible to use infer declarations in constraint clauses for regular type parameters:

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

但是,通过删除约束中的类型变量并改为指定条件类型,可以获得大致相同的效果:

🌐 However, much the same effect can be obtained by erasing the type variables in the constraint and instead specifying a conditional type:

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 中添加了几个预定义的条件类型:

🌐 TypeScript 2.8 adds several predefined conditional types to lib.d.ts:

  • Exclude<T, U> — 从 T 中排除那些可以分配给 U 的类型。
  • Extract<T, U> — 从 T 中提取可分配给 U 的类型。
  • NonNullable<T> — 从 T 中排除 nullundefined
  • ReturnType<T> — 获取函数类型的返回类型。
  • InstanceType<T> — 获取构造函数类型的实例类型。
示例

🌐 Example

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? 修饰符,但它们不支持删除修饰符的功能。这在同构映射类型中很重要,因为默认情况下它们会保留底层类型的修饰符。

🌐 Mapped types support adding a readonly or ? modifier to a mapped property, but they did not provide support for the ability to remove modifiers. This matters in homomorphic mapped types which by default preserve the modifiers of the underlying type.

TypeScript 2.8 增加了映射类型可以添加或移除特定修饰符的能力。具体来说,现在在映射类型中,readonly? 属性修饰符可以前置 +-,以指示该修饰符应该被添加或移除。

🌐 TypeScript 2.8 adds the ability for a mapped type to either add or remove a particular modifier. Specifically, a readonly or ? property modifier in a mapped type can now be prefixed with either + or - to indicate that the modifier should be added or removed.

示例

🌐 Example

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> 类型对应于

🌐 A modifier with no + or - prefix is the same as a modifier with a + prefix. So, the ReadonlyPartial<T> type above corresponds to

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

使用此功能后,lib.d.ts 现在有了一个新的 Required<T> 类型。此类型会从 T 的所有属性中移除 ? 修饰符,从而使所有属性变为必填。

🌐 Using this ability, lib.d.ts now has a new Required<T> type. This type strips ? modifiers from all properties of T, thus making all properties required.

示例

🌐 Example

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

请注意,在 strictNullChecks 模式下,当同态映射类型从底层类型的某个属性中移除 ? 修饰符时,它也会从该属性的类型中移除 undefined

🌐 Note that in strictNullChecks mode, when a homomorphic mapped type removes a ? modifier from a property in the underlying type it also removes undefined from the type of that property:

示例

🌐 Example

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 表达式推断的不一致问题。

🌐 With TypeScript 2.8 keyof applied to an intersection type is transformed to a union of keyof applied to each intersection constituent. In other words, types of the form keyof (A & B) are transformed to be keyof A | keyof B. This change should address inconsistencies with inference from keyof expressions.

示例

🌐 Example

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 中也被识别为命名空间声明。

🌐 TypeScript 2.8 adds support for understanding more namespace patterns in .js files. Empty object literals declarations on top level, just like functions and classes, are now recognized as namespace declarations in JavaScript.

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

顶层的赋值应该表现一致;换句话说,不需要 varconst 声明。

🌐 Assignments at the top-level should behave the same way; in other words, a var or const declaration is not required.

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

IIFE 作为命名空间声明

🌐 IIFEs as namespace declarations

返回函数、类或空对象字面量的 IIFE 也被识别为命名空间:

🌐 An IIFE returning a function, class or empty object literal, is also recognized as a namespace:

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 属性。单独的 prototype 分配同样有效:

🌐 You can assign an object literal directly to the prototype property. Individual prototype assignments still work too:

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

现在嵌套可以达到任意层级,并且能够正确跨文件合并。之前两者都不行。

🌐 Nesting works to any level now, and merges correctly across files. Previously neither was the case.

js
var app = window.app || {};
app.C = class {};

每个文件的 JSX 工厂

🌐 Per-file JSX factories

TypeScript 2.8 添加了对每个文件可配置的 JSX 工厂名称的支持,可以使用 @jsx dom 指令。JSX 工厂可以通过 jsxFactory 设置用于编译(默认值是 React.createElement)。在 TypeScript 2.8 中,你可以通过在文件开头添加注释来按文件覆盖此设置。

🌐 TypeScript 2.8 adds support for a per-file configurable JSX factory name using @jsx dom pragma. JSX factory can be configured for a compilation using jsxFactory (default is React.createElement). With TypeScript 2.8 you can override this on a per-file-basis by adding a comment to the beginning of the file.

示例

🌐 Example

ts
/** @jsx dom */
import { dom } from "./renderer";
<h></h>;

生成:

🌐 Generates:

js
var renderer_1 = require("./renderer");
renderer_1.dom("h", null);

本地作用域的 JSX 命名空间

🌐 Locally scoped JSX namespaces

JSX 类型检查依赖于 JSX 命名空间中的定义,例如 JSX.Element 用于 JSX 元素的类型,JSX.IntrinsicElements 用于内置元素。在 TypeScript 2.8 之前,JSX 命名空间需要存在于全局命名空间中,因此在一个项目中只能定义一个。从 TypeScript 2.8 开始,JSX 命名空间将会在 jsxNamespace(例如 React)下查找,从而允许在一次编译中存在多个 JSX 工厂。为了向后兼容,如果在工厂函数上没有定义,则会使用全局 JSX 命名空间作为回退。结合每个文件的 @jsx 指令,每个文件可以使用不同的 JSX 工厂。

🌐 JSX type checking is driven by definitions in a JSX namespace, for instance JSX.Element for the type of a JSX element, and JSX.IntrinsicElements for built-in elements. Before TypeScript 2.8 the JSX namespace was expected to be in the global namespace, and thus only allowing one to be defined in a project. Starting with TypeScript 2.8 the JSX namespace will be looked under the jsxNamespace (e.g. React) allowing for multiple jsx factories in one compilation. For backward compatibility the global JSX namespace is used as a fallback if none was defined on the factory function. Combined with the per-file @jsx pragma, each file can have a different JSX factory.

新的 --emitDeclarationOnly

🌐 New --emitDeclarationOnly

emitDeclarationOnly 仅允许生成声明文件;使用此标志时,.js/.jsx 输出生成将被跳过。 当 .js 输出生成由像 Babel 这样的不同编译器处理时,该标志非常有用。