Skip to content
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待

TypeScript 2.8

条件类型

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

ts
T extends U ? X : Y

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

条件类型 T extends U ? X : Y 要么被解析XY,要么被延迟,因为条件依赖于一个或多个类型变量。 决定是解析还是延迟的规则如下:

  • 首先,给定类型 T'U',它们是 TU 的实例化,其中所有类型参数的出现都被替换为 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"

分布式条件类型

其中被检查类型是一个裸类型参数的条件类型称为分布式条件类型。 分布式条件类型在实例化时自动分布在联合类型上。 例如,以类型参数 TA | B | CT extends U ? X : Y 的实例化被解析为 (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(即,在 XT 被认为可赋值给 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>;

注意,在 Boxed<T> 的 true 分支中,T 有额外的约束 any[],因此可以引用数组的元素类型为 T[number]。还要注意,在最后一个示例中,条件类型是如何分布在联合类型上的。

条件类型的分布式特性可以方便地用于过滤联合类型:

ts
type Diff<T, U> = T extends U ? never : T; // 从 T 中移除可赋值给 U 的类型
type Filter<T, U> = T extends U ? T : never; // 从 T 中移除不可赋值给 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>; // 从 T 中移除 null 和 undefined

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; // 可以
  y = x; // 错误
}

function f2<T extends string | undefined>(x: T, y: NonNullable<T>) {
  x = y; // 可以
  y = x; // 错误
  let s1: string = x; // 错误
  let s2: string = y; // 可以
}

条件类型与映射类型结合使用时特别有用:

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; // 错误

条件类型中的类型推断

在条件类型的 extends 子句中,现在可以使用 infer 声明来引入一个待推断的类型变量。 这种推断出的类型变量可以在条件类型的 true 分支中引用。 同一个类型变量可以有多个 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; // 错误,不支持

但是,可以通过擦除约束中的类型变量并指定条件类型来获得几乎相同的效果:

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

预定义条件类型

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>; // 错误
type T18 = ReturnType<Function>; // 错误

type T20 = InstanceType<typeof C>; // C
type T21 = InstanceType<any>; // any
type T22 = InstanceType<never>; // any
type T23 = InstanceType<string>; // 错误
type T24 = InstanceType<Function>; // 错误

注意:Exclude 类型是此处建议的 Diff 类型的正确实现。我们使用了名称 Exclude 以避免破坏定义了 Diff 的现有代码,并且我们认为这个名称更好地传达了类型的语义。我们没有包含 Omit<T, K> 类型,因为它可以简单地写为 Pick<T, Exclude<keyof T, K>>

改进对映射类型修饰符的控制

映射类型支持向映射属性添加 readonly? 修饰符,但它们不支持移除修饰符的能力。 这在默认保留底层类型修饰符的同态映射类型中很重要。

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

示例

ts
type MutableRequired<T> = { -readonly [P in keyof T]-?: T[P] }; // 移除 readonly 和 ?
type ReadonlyPartial<T> = { +readonly [P in keyof T]+?: T[P] }; // 添加 readonly 和 ?

没有 +- 前缀的修饰符与带有 + 前缀的修饰符相同。因此,上面的 ReadonlyPartial<T> 类型对应于

ts
type ReadonlyPartial<T> = { readonly [P in keyof T]?: T[P] }; // 添加 readonly 和 ?

利用这种能力,lib.d.ts 现在有一个新的 Required<T> 类型。 此类型从 T 的所有属性中移除 ? 修饰符,从而使所有属性变为必需。

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

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

示例
ts
type Foo = { a?: string }; // 等同于 { a?: string | undefined }
type Bar = Required<Foo>; // 等同于 { a: string }

改进的交集类型 keyof

在 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 文件中更好地处理命名空间模式

TypeScript 2.8 增加了对理解 .js 文件中更多命名空间模式的支持。 顶层上的空对象字面量声明,就像函数和类一样,现在在 JavaScript 中被识别为命名空间声明。

js
var ns = {}; // 被识别为命名空间 `ns` 的声明
ns.constant = 1; // 被识别为变量 `constant` 的声明

顶层的赋值应该以同样的方式表现;换句话说,不需要 varconst 声明。

js
app = {}; // 不需要是 `var app = {}`
app.C = class {};
app.f = function() {};
app.prop = 1;

作为命名空间声明的 IIFE

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

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

默认声明

“默认声明”允许初始化器引用逻辑或左侧的声明名称:

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

原型赋值

你可以直接将对象字面量赋值给 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;
};

嵌套和合并声明

现在嵌套可以到任何级别,并且在文件之间正确合并。以前这两种情况都不支持。

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

每个文件的 JSX 工厂

TypeScript 2.8 增加了使用 @jsx dom 编译指示为每个文件配置 JSX 工厂名称的支持。 可以使用 jsxFactory(默认为 React.createElement)为编译配置 JSX 工厂。使用 TypeScript 2.8,你可以通过向文件开头添加注释来在每个文件的基础上覆盖它。

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

生成:

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

局部作用域的 JSX 命名空间

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

新的 --emitDeclarationOnly

emitDeclarationOnly 允许生成声明文件;使用此标志将跳过 .js/.jsx 输出生成。当 .js 输出生成由不同的转译器(如 Babel)处理时,此标志很有用。