TypeScript 2.0

编译器理解 null 和 undefined 类型[2]

TypeScript 有两个特殊的类型, Null 和 Undefined, 他们分别对应了值 nullundefined. 过去这些类型没有明确的名称, 但 nullundefined 现在可以在任意类型检查模式下作为类型名称使用.

类型检查系统过去认为 nullundefined 可以赋值给任何东西. 或者说, nullundefined 是任何类型的合法值, 并且之前无法将他们排除 (由此也无法检测相关的错误使用).

--strictNullChecks

--strictNullChecks 选项会启用新的严格空值检查模式.

在严格空值检查模式下, 值 nullundefined 不再属与所有类型并且只能赋值给它们自己对应的类型和 any (一个例外是 undefined 也可以被复制给 void). 所以, 虽然在普通类型检查模式 TT | undefined 意义相同 (因为 undefined 被认为是任何 T 的子类型), 在严格类型检查模式下它们是不同的, 并且只有 T | undefined 允许 undefined 作为值. TT | null 的关系也是如此.

例子
  1. // 使用 --strictNullChecks 选项编译
  2. let x: number;
  3. let y: number | undefined;
  4. let z: number | null | undefined;
  5. x = 1; // 正确
  6. y = 1; // 正确
  7. z = 1; // 正确
  8. x = undefined; // 错误
  9. y = undefined; // 正确
  10. z = undefined; // 正确
  11. x = null; // 错误
  12. y = null; // 错误
  13. z = null; // 正确
  14. x = y; // 错误
  15. x = z; // 错误
  16. y = x; // 正确
  17. y = z; // 错误
  18. z = x; // 正确
  19. z = y; // 正确

使用前赋值检查

在严格空值检查模式下, 编译器要求每个类型中不包含 undefined 的本地变量的引用在它之前的代码路径里被赋值.

例子
  1. // 使用 --strictNullChecks 选项编译
  2. let x: number;
  3. let y: number | null;
  4. let z: number | undefined;
  5. x; // 错误, 引用使用前没有被赋值
  6. y; // 错误, 引用使用前没有被赋值
  7. z; // 正确
  8. x = 1;
  9. y = null;
  10. x; // 正确
  11. y; // 正确

可选参数和属性

可选参数和属性类型会自动包含 undefined, 即便它们的类型标注没有明确包含 undefined. 举个例子, 下面的两个类型是等价的:

  1. // 使用 --strictNullChecks 选项编译
  2. type T1 = (x?: number) => string; // x 的类型为 number | undefined
  3. type T2 = (x?: number | undefined) => string; // x 的类型为 number | undefined

非 null 和非 undefined 类型收窄

如果一个对象的类型包含了 null 或者 undefined, 访问它的属性或者调用它的方法会产生编译时错误. 然而, 类型收窄已经支持了非 null 和非 undefined 检查.

例子
  1. // 使用 --strictNullChecks 选项编译
  2. declare function f(x: number): string;
  3. let x: number | null | undefined;
  4. if (x) {
  5. f(x); // 正确, x 的类型在这里是 number
  6. }
  7. else {
  8. f(x); // 错误, x 的类型在这里是 number?
  9. }
  10. let a = x != null ? f(x) : ""; // a 的类型为 string
  11. let b = x && f(x); // b 的类型为 string | 0 | null | undefined

非 null 和非 undefined 类型收窄可以使用 ==, !=, === 或者 !== 运算符与 null 或者 undefined 进行比较, 比如 x != null 或者 x === undefined. 对目标变量类型的效果能准确反映 JavaScript 的语义 (比如双等号运算符两个值都会检查, 而三等号只会检查指定的值).

类型收窄中带点的名称

类型收窄过去只支持对本地变量和参数的检查. 现在类型收窄支持检查包含了带一级或多级属性访问的变量或参数的 “带点的名称”.

例子
  1. interface Options {
  2. location?: {
  3. x?: number;
  4. y?: number;
  5. };
  6. }
  7. function foo(options?: Options) {
  8. if (options && options.location && options.location.x) {
  9. const x = options.location.x; // x 的类型为 number
  10. }
  11. }

对于带点名称的类型收窄也可以和用户自定义的类型收窄函数及 typeofinstanceof 运算符一起使用, 并且不依赖 --strictNullChecks 编译器选项.

带点名称的类型收窄如果跟着对其任意部分的赋值, 会使收窄无效. 比如说, 对于 x.y.z 的类型收窄如果后面跟着对 x, x.y 或者 x.y.z 的赋值都会使其无效.

表达式运算符

表达式运算符允许操作数的类型包含 null 和/或 undefined, 但会总是产生类型为非 null 和非 undefined 的值.

  1. // 使用 --strictNullChecks 选项编译
  2. function sum(a: number | null, b: number | null) {
  3. return a + b; // 产生类型为 number 的值
  4. }

&& 运算符会根据左边操作数的类型将 null 和/或 undefined 添加到右边被操作数的类型上, 而 || 运算符会在结果的联合类型中同时移除左边被操作数类型中的 nullundefined.

  1. // 使用 --strictNullChecks 选项编译
  2. interface Entity {
  3. name: string;
  4. }
  5. let x: Entity | null;
  6. let s = x && x.name; // s 的类型为 string | null
  7. let y = x || { name: "test" }; // y 的类型为 Entity

类型拓宽

nullundefined 类型在严格空值检查模式中不会被拓宽为 any.

  1. let z = null; // z 的类型为 null

在普通的类型检查模式中, 会因为拓宽将 z 的类型推断为 any, 但在严格空值检查模式中, z 推断得出的类型是 null (也正因为如此, 在没有类型标注的情况下, nullz 唯一可能的值).

非空断言运算符

在类型检查器无法得出与实际相符的结论的上下文中, 可以使用新的 ! 后缀表达式运算符来声明它的操作数是非 null 和非 undefined 的. 特别的, 运算 x! 会得出 x 排除 nullundefined 后的类型的值. 与 <T>xx as T 这种形式的类型断言相似, ! 非空断言运算符在输出的 JavaScript 代码中会被直接移除.

  1. // 使用 --strictNullChecks 选项编译
  2. function validateEntity(e?: Entity) {
  3. // 当 e 是 null 或非法 entity 时抛出异常
  4. }
  5. function processEntity(e?: Entity) {
  6. validateEntity(e);
  7. let s = e!.name; // 声明 e 非 null 并且访问 name
  8. }

兼容性

这些新的特性在设计的时候考虑了严格空值检查模式和常规类型检查模式下的使用. 比如 nullundefined 类型在常规类型检查模式下会从联合类型中自动清除 (因为它们是其他任何类型的子类型), 而 ! 非空断言运算符虽然允许在常规类型检查模式中使用, 但不会有效果. 于是, 更新后支持可为 null 或者 undefined 类型的声明文件为了向后兼容依然可以在常规类型检查模式下使用.

实际使用时, 严格的空值检查模式需要所有被编译的文件包含是否可为 null 或 undefined 的信息.

基于控制流的类型分析

TypeScript 2.0 实现了对于本地变量和参数基于控制流的类型分析. 过去对类型收窄进行的分析仅限于 if 语句和 ?: 条件表达式, 并没有包含赋值和控制流结构带来的影响, 比如 returnbreak 语句. 在 TypeScript 2.0 中, 类型检查器会分析语句和表达式中所有可能的控制流, 以尽可能得出包含联合类型的本地变量及参数在指定位置最准确的类型 (收窄的类型).

例子
  1. function foo(x: string | number | boolean) {
  2. if (typeof x === "string") {
  3. x; // x 的类型在这里是 string
  4. x = 1;
  5. x; // x 的类型在这里是 number
  6. }
  7. x; // x 的类型在这里是 number | boolean
  8. }
  9. function bar(x: string | number) {
  10. if (typeof x === "number") {
  11. return;
  12. }
  13. x; // x 的类型在这里是 string
  14. }

基于控制流的类型分析在 --strictNullChecks 模式下非常重要, 因为可空的类型是使用联合类型表示的:

  1. function test(x: string | null) {
  2. if (x === null) {
  3. return;
  4. }
  5. x; // x 在这之后的类型为 string
  6. }

此外, 在 --strictNullChecks 模式, 基于控制流的类型分析包含了对类型不允许值 undefined 的本地变量的明确赋值分析.

  1. function mumble(check: boolean) {
  2. let x: number; // 类型不允许 undefined
  3. x; // 错误, x 是 undefined
  4. if (check) {
  5. x = 1;
  6. x; // 正确
  7. }
  8. x; // 错误, x 可能是 undefined
  9. x = 2;
  10. x; // 正确
  11. }

带标记的联合类型

TypeScript 2.0 实现了对带标记的 (或被区别的) 联合类型. 特别的, TS 编译器现在支持通过对可识别的属性进行判断来收窄联合类型, 并且支持 switch 语句.

例子
  1. interface Square {
  2. kind: "square";
  3. size: number;
  4. }
  5. interface Rectangle {
  6. kind: "rectangle";
  7. width: number;
  8. height: number;
  9. }
  10. interface Circle {
  11. kind: "circle";
  12. radius: number;
  13. }
  14. type Shape = Square | Rectangle | Circle;
  15. function area(s: Shape) {
  16. // 在下面的语句中, s 的类型在每一个分支中都被收窄了
  17. // 根据可识别属性的值, 允许变量的其他属性在没有类型断言的情况下被访问
  18. switch (s.kind) {
  19. case "square": return s.size * s.size;
  20. case "rectangle": return s.width * s.height;
  21. case "circle": return Math.PI * s.radius * s.radius;
  22. }
  23. }
  24. function test1(s: Shape) {
  25. if (s.kind === "square") {
  26. s; // Square
  27. }
  28. else {
  29. s; // Rectangle | Circle
  30. }
  31. }
  32. function test2(s: Shape) {
  33. if (s.kind === "square" || s.kind === "rectangle") {
  34. return;
  35. }
  36. s; // Circle
  37. }

可识别属性的类型收窄可以是如下的表达式 x.p == v, x.p === v, x.p != v, 或 x.p !== v, 其中 pv 分别是一个属性和一个为字符串字面量类型或字符串字面量类型的联合类型的表达式.
可识别属性的类型收窄会将 x 的类型收窄到包含的可识别属性 pv 一个可能的值的 x 的部分类型.

注意我们现在仅支持字符串字面量类型的可识别属性.
我们打算在以后增加布尔值和数字的字面类型.

never 类型

TypeScript 2.0 引入了新的原始类型 never.
never 类型代表从来不会出现的值的类型.
特别的, never 可以是永远不返回的函数的返回值类型, 也可以是变量在类型收窄中不可能为真的类型.

never 类型有以下特征:

  • never 是任何类型的子类型, 并且可以赋值给任何类型.
  • 没有类型是 never 的子类型或者可以复制给 never (除了 never 本身).
  • 在一个没有返回值标注的函数表达式或箭头函数中, 如果函数没有 return 语句, 或者仅有表达式类型为 neverreturn 语句, 并且函数的终止点无法被执行到 (按照控制流分析), 则推导出的函数返回值类型是 never.
  • 在一个明确指定了 never 返回值类型的函数中, 所有 return 语句 (如果有) 表达式的值必须为 never 类型, 且函数不应能执行到终止点.

由于 never 是所有类型的子类型, 在联合类型中它始终被省略, 并且只要函数有其他返回的类型, 推导出的函数返回值类型中就会忽略它.

一些返回 never 的函数的例子:

  1. // 返回 never 的函数必须有无法被执行到的终止点
  2. function error(message: string): never {
  3. throw new Error(message);
  4. }
  5. // 推断的返回值是 never
  6. function fail() {
  7. return error("一些东西失败了");
  8. }
  9. // 返回 never 的函数必须有无法被执行到的终止点
  10. function infiniteLoop(): never {
  11. while (true) {
  12. }
  13. }

一些使用返回 never 的函数的例子:

  1. // 推断的返回值类型为 number
  2. function move1(direction: "up" | "down") {
  3. switch (direction) {
  4. case "up":
  5. return 1;
  6. case "down":
  7. return -1;
  8. }
  9. return error("永远不应该到这里");
  10. }
  11. // 推断的返回值类型为 number
  12. function move2(direction: "up" | "down") {
  13. return direction === "up" ? 1 :
  14. direction === "down" ? -1 :
  15. error("永远不应该到这里");
  16. }
  17. // 推断的返回值类型为 T
  18. function check<T>(x: T | undefined) {
  19. return x || error("未定义的值");
  20. }

因为 never 可以赋值给任何类型, 返回 never 的函数可以在回调需要返回一个具体类型的时候被使用:

  1. function test(cb: () => string) {
  2. let s = cb();
  3. return s;
  4. }
  5. test(() => "hello");
  6. test(() => fail());
  7. test(() => { throw new Error(); })

只读属性和索引签名

现在结合 readonly 修饰符声明的属性或者索引签名会被认为是只读的.

只读属性可以有初始化语句, 并且可以在当前类声明的构造函数中被赋值, 但对于只读属性其他形式的赋值是不被允许的.

另外, 在一些情况下, 实体是隐式只读的:

  • 一个只有 get 访问符没有 set 访问符声明的属性被认为是只读的.
  • 在枚举对象的类型中, 枚举成员被认为是只读属性.
  • 在模块对象的类型中, 导出的 const 变量被认为是只读属性.
  • import 语句中声明的实体被认为是只读的.
  • 通过 ES2015 命名空间导入来访问的实体被认为是只读的 (比如当 fooimport * as foo from "foo" 声明时, foo.x 是只读的).
例子
  1. interface Point {
  2. readonly x: number;
  3. readonly y: number;
  4. }
  5. var p1: Point = { x: 10, y: 20 };
  6. p1.x = 5; // 错误, p1.x 是只读的
  7. var p2 = { x: 1, y: 1 };
  8. var p3: Point = p2; // 正确, p2 的只读别名
  9. p3.x = 5; // 错误, p3.x 是只读的
  10. p2.x = 5; // 正确, 但由于是别名也会改变 p3.x
  1. class Foo {
  2. readonly a = 1;
  3. readonly b: string;
  4. constructor() {
  5. this.b = "hello"; // 在构造函数中允许赋值
  6. }
  7. }
  1. let a: Array<number> = [0, 1, 2, 3, 4];
  2. let b: ReadonlyArray<number> = a;
  3. b[5] = 5; // 错误, 元素是只读的
  4. b.push(5); // 错误, 没有 push 方法 (因为它会改变数组)
  5. b.length = 3; // 错误, length 是只读的
  6. a = b; // 错误, 缺少会改变数组值的方法

指定函数的 this 类型

在指定类和接口中 this 的类型之后, 函数和方法现在可以声明它们期望的 this 类型了.

函数中 this 的类型默认为 any.
从 TypeScript 2.0 开始, 你可以添加一个明确地 this 参数.
this 参数是位于函数参数列表开头的假参数.

  1. function f(this: void) {
  2. // 确保 `this` 在这个单独的函数中不可用
  3. }

回调函数中的 this 参数

库也可以使用 this 参数来声明回调会如何被调用.

例子
  1. interface UIElement {
  2. addClickListener(onclick: (this: void, e: Event) => void): void;
  3. }

this: void 说明 addClickListener 期望 onclick 是一个不要求 this 类型的函数.

现在如果你标注 this 的调用代码:

  1. class Handler {
  2. info: string;
  3. onClickBad(this: Handler, e: Event) {
  4. // 啊哦, 这里使用了 this. 把它用作回调函数可能会在运行时崩掉
  5. this.info = e.message;
  6. };
  7. }
  8. let h = new Handler();
  9. uiElement.addClickListener(h.onClickBad); // 错误!

--noImplicitThis

在 TypeScript 2.0 中也加入了一个新的选项来检查函数中没有明确标注类型的 this 的使用.

tsconfig.json 对 glob 的支持

支持 glob 啦!! 对 glob 的支持一直是呼声最高的特性之一.

类似 glob 的文件匹配模板现在被 "include""exclude" 两个属性支持.

例子
  1. {
  2. "compilerOptions": {
  3. "module": "commonjs",
  4. "noImplicitAny": true,
  5. "removeComments": true,
  6. "preserveConstEnums": true,
  7. "outFile": "../../built/local/tsc.js",
  8. "sourceMap": true
  9. },
  10. "include": [
  11. "src/**/*"
  12. ],
  13. "exclude": [
  14. "node_modules",
  15. "**/*.spec.ts"
  16. ]
  17. }

支持的 glob 通配符有:

  • * 匹配零个或多个字符 (不包括目录分隔符)
  • ? 匹配任意一个字符 (不包括目录分隔符)
  • **/ 递归匹配子目录

如果 glob 模板的一个片段只包含 *.*, 那么只有扩展名被支持的文件会被包含 (比如默认有 .ts, .tsx.d.ts, 如果 allowJs 被启用的话还有 .js.jsx).

如果 "files""include" 都没有指定, 编译器会默认包含当前目录及其子目录中除开由 "exclude" 属性排除的以外所有 TypeScript (.ts, .d.ts.tsx) 文件. 如果 allowJs 被开启, JS 文件 (.js.jsx) 也会被包含.

如果指定了 "files""include" 属性, 编译器便会包含两个属性指定文件的并集.
除非明确地通过 "files" 属性指定 (即便指定了 "exclude" 属性), 在使用 "outDir" 编译器选项指定目录中的文件总是会被排除.

使用 "include" 包含的文件可以使用 "exclude" 属性排除.
然而, 由 "files" 属性明确包含的文件总是会被包含, 不受 "exclude" 影响.
如果没有指定, "exclude" 属性默认会排除 node_modules, bower_components 以及 jspm_packages 目录.

模块解析增强: 基准 URL, 路径映射, 多个根目录及追踪

TypeScript 2.0 提供了一系列模块解析配置来告知编译器从哪里找到给定模块的声明.

模块解析文档了解更多内容.

基准 URL

对于使用 AMD 模块加载器将模块在运行时 “部署” 到单个文件夹的应用来说, 使用 baseUrl 是一个通用的做法.
所有没有相对名称的模块引用都被假设为相对 baseUrl.

例子
  1. {
  2. "compilerOptions": {
  3. "baseUrl": "./modules"
  4. }
  5. }

现在对 "moduleA" 的导入会查找 ./modules/moduleA

  1. import A from "moduleA";

路径映射

有时模块没有直接在 baseUrl 中.
加载器使用映射配置将模块名称与文件在运行时对应起来, 见 RequireJS 文档SystemJS 文档.

TypeScript 编译器支持在 tsconfig.json 里使用 "path" 属性来声明这样的映射.

例子

举例来说, 导入模块 "jquery" 会编译为运行时的 "node_modules/jquery/dist/jquery.slim.min.js".

  1. {
  2. "compilerOptions": {
  3. "paths": {
  4. "jquery": ["jquery/dist/jquery.slim.min"]
  5. }
  6. }
  7. }

通过 "path" 还可以实现更多包括多级回落路径在内的高级映射.
考虑一个部分模块在一个位置, 其他的在另一个位置的项目配置.

使用 rootDirs 配置虚拟路径

你可以使用 rootDirs 告知编译器组成这个 “虚拟” 路径的多个根路径;
这样一来, 编译器会将这些 “虚拟” 目录中的相对模块引入当做它们是被合并到同一个目录下的来解析.

例子

假定项目结构如下:

  1. src
  2. └── views
  3. └── view1.ts (imports './template1')
  4. └── view2.ts
  5. generated
  6. └── templates
  7. └── views
  8. └── template1.ts (imports './view2')

构建步骤会将 /src/views/generated/templates/views 中的文件在输出中复制到同一目录.
在运行时, 一个视图会期望它的模板在它旁边, 这样就应该使用相对的名称 "./template" 来导入它.

"rootDirs" 指定了将在运行时合并的多个根目录的列表.
所以对于我们的例子, tsconfig.json 文件应该像这样:

  1. {
  2. "compilerOptions": {
  3. "rootDirs": [
  4. "src/views",
  5. "generated/templates/views"
  6. ]
  7. }
  8. }

追踪模块解析

--traceResolution 提供了一个方便的途径帮助理解模块是如何被编译器解析的.

  1. tsc --traceResolution

简易外围模块声明

现在如果你不想在使用一个新的模块前花时间写声明信息, 你可以直接使用一个简易声明迅速开始.

declarations.d.ts
  1. declare module "hot-new-module";

所有从简易模块中导入的项都为 any 类型.

  1. import x, {y} from "hot-new-module";
  2. x(y);

模块名中的通配符

从模块加载器扩展 (比如 AMD 或者 SystemJS) 中导入非代码资源过去并不容易;
在这之前每一个资源都必须定义对应的外围模块声明.

TypeScript 2.0 支持使用通配符 (*) 来声明一组模块名称;
这样, 声明一次就可以对应一组扩展, 而不是单个资源.

例子
  1. declare module "*!text" {
  2. const content: string;
  3. export default content;
  4. }
  5. // 有的使用另一种方式
  6. declare module "json!*" {
  7. const value: any;
  8. export default value;
  9. }

现在你就可以导入匹配 "*!text" 或者 "json!*" 的东西了.

  1. import fileContent from "./xyz.txt!text";
  2. import data from "json!http://example.com/data.json";
  3. console.log(data, fileContent);

通配符模块名在从无类型的代码迁移的过程中会更加有用.
结合简易模块声明, 一组模块可以轻松地被声明为 any.

例子
  1. declare module "myLibrary/*";

所有对 myLibrary 下的模块的导入会被编译器认为是 any 类型;
这样, 对于这些模块的外形或类型的检查都会被关闭.

  1. import { readFile } from "myLibrary/fileSystem/readFile";
  2. readFile(); // readFile 的类型是 any

支持 UMD 模块定义

一些库按设计可以在多个模块加载器中使用, 或者不使用模块加载 (全局变量).
这种方式被叫做 UMD 或者同构模块.
这些库既可以使用模块导入, 也可以通过全局变量访问.

比如:

math-lib.d.ts
  1. export const isPrime(x: number): boolean;
  2. export as namespace mathLib;

这个库现在就可以在模块中作为导入项被使用:

  1. import { isPrime } from "math-lib";
  2. isPrime(2);
  3. mathLib.isPrime(2); // 错误: 在模块中不能使用全局定义

它也可以被用作全局变量, 但是仅限于脚本中.
(脚本是指不包含导入项或者导出项的文件.)

  1. mathLib.isPrime(2);

可选的类属性

可选的类属性和方法现在可以在类中被声明了, 与之前已经在接口中允许的相似.

例子
  1. class Bar {
  2. a: number;
  3. b?: number;
  4. f() {
  5. return 1;
  6. }
  7. g?(): number; // 可选方法的函数体可以被省略
  8. h?() {
  9. return 2;
  10. }
  11. }

当在 --strictNullChecks 模式下编译时, 可选属性和方法会自动在其类型中包含 undefined. 如此, 上面的 b 属性类型为 number | undefined, g 方法类型为 (() => number) | undefined.
类型收窄可以被用来排除这些类型中的 undefined 部分:

  1. function test(x: Bar) {
  2. x.a; // number
  3. x.b; // number | undefined
  4. x.f; // () => number
  5. x.g; // (() => number) | undefined
  6. let f1 = x.f(); // number
  7. let g1 = x.g && x.g(); // number | undefined
  8. let g2 = x.g ? x.g() : 0; // number
  9. }

私有和受保护的构造函数

类的构造函数可以被标记为 privateprotected.
一个有私有构造函数的类不能在类定义以外被实例化, 并且不能被扩展.
一个有受保护构造函数的类不能在类定义以外被实例化, 但是可以被扩展.

例子
  1. class Singleton {
  2. private static instance: Singleton;
  3. private constructor() { }
  4. static getInstance() {
  5. if (!Singleton.instance) {
  6. Singleton.instance = new Singleton();
  7. }
  8. return Singleton.instance;
  9. }
  10. }
  11. let e = new Singleton(); // 错误: 'Singleton' 的构造函数是私有的.
  12. let v = Singleton.getInstance();

抽象属性和访问器

一个抽象类可以声明抽象的属性和/或访问器.
一个子类需要声明这些抽象的属性或者被标记为抽象的.
抽象的属性不能有初始值.
抽象的访问器不能有定义.

例子
  1. abstract class Base {
  2. abstract name: string;
  3. abstract get value();
  4. abstract set value(v: number);
  5. }
  6. class Derived extends Base {
  7. name = "derived";
  8. value = 1;
  9. }

隐式索引签名

现在如果一个对象字面量中所有已知的属性可以赋值给一个索引签名, 那么这个对象字面量类型就可以赋值给有该索引签签名的类型. 这使得将使用对象字面量初始化的变量可以作为参数传递给接受映射或字典的函数:

  1. function httpService(path: string, headers: { [x: string]: string }) { }
  2. const headers = {
  3. "Content-Type": "application/x-www-form-urlencoded"
  4. };
  5. httpService("", { "Content-Type": "application/x-www-form-urlencoded" }); // 正确
  6. httpService("", headers); // 现在可以, 过去不行

使用 --lib 加入内建类型声明

过去使用 ES6/2015 的内建 API 声明仅限于 target: ES6.
输入 --lib; 通过 --lib 你可以指定你希望在项目中包含的内建 API 声明组.
举例来说, 如果你期望运行时支持 Map, SetPromise (比如目前较新的浏览器), 只需要加入 --lib es2015.collection,es2015.promise.
相似的, 你可以从你的项目中排除你不希望加入的声明, 比如在使用 --lib es5,es6 选项的 node 项目中排除 DOM.

这里列出了可选的 API 组:

  • dom
  • webworker
  • es5
  • es6 / es2015
  • es2015.core
  • es2015.collection
  • es2015.iterable
  • es2015.promise
  • es2015.proxy
  • es2015.reflect
  • es2015.generator
  • es2015.symbol
  • es2015.symbol.wellknown
  • es2016
  • es2016.array.include
  • es2017
  • es2017.object
  • es2017.sharedmemory
  • scripthost
例子
  1. tsc --target es5 --lib es5,es6.promise
  1. "compilerOptions": {
  2. "lib": ["es5", "es2015.promise"]
  3. }

使用 --noUnusedParameters--noUnusedLocals 标记未使用的声明

TypeScript 2.0 提供了两个新的选项来帮助你保持代码整洁.
--noUnusedParameters 可以标记所有未使用的函数或方法参数为错误.
--noUnusedLocals 可以标记所有未使用的本地 (未导出的) 声明, 比如变量, 函数, 类, 导入项等等.
另外, 类中未使用的私有成员也会被 --noUnusedLocals 标记为错误.

例子
  1. import B, { readFile } from "./b";
  2. // ^ 错误: `B` 被声明但从未使用
  3. readFile();
  4. export function write(message: string, args: string[]) {
  5. // ^^^^ 错误: `arg` 被声明但从未使用
  6. console.log(message);
  7. }

名称由 _ 开头的参数声明不会被检查是否未使用.
比如:

  1. function returnNull(_a) { // 正确
  2. return null;
  3. }

模块名称允许 .js 扩展名

在 TypeScript 2.0 之前, 模块名称一直被假设是没有扩展名的;
举例来说, 对于模块导入 import d from "./moduleA.js", 编译器会到 ./moduleA.js.ts./moduleA.js.d.ts 中查找 "moduleA.js" 的声明.
这让使用像 SystemJS 这样期望模块名是 URI 的打包/加载工具变得不方便.

对于 TypeScript 2.0, 编译器会到 ./moduleA.ts./moduleA.d.ts 中查找 "moduleA.js" 的声明.

支持同时使用 target: es5module: es6

之前这被认为是不合法的选项组合, 现在 target: es5module: es6 可以一并使用了.
这可以方便使用基于 ES2015 的冗余代码清理工具, 比如 rollup.

函数形参和实参列表结尾处的逗号

现在允许了函数形参和实参列表结尾处的逗号.
这是一个第三阶段的 ECMAScript 提议对应的实现, 并且会编译为可用的 ES3/ES5/ES6.

例子
  1. function foo(
  2. bar: Bar,
  3. baz: Baz, // 形参列表可以以逗号结尾
  4. ) {
  5. // 实现...
  6. }
  7. foo(
  8. bar,
  9. baz, // 实参列表也可以
  10. );

新的 --skipLibCheck

TypeScript 2.0 添加了一个新的 --skipLibCheck 编译器选项来跳过对声明文件 (扩展名为 .d.ts 的文件) 的类型检查.
当程序包含了大的声明文件时, 编译器会花掉很多时间对这些已知没有错误的声明进行类型检查, 跳过这些声明文件的类型检查能够显著缩短编译时间.

由于一个文件中的声明可能影响其他文件中的类型检查, 指定 --skipLibCheck 可能导致一些错误无法被检测到.
比如说, 如果一个非声明文件中的类型被声明文件用到, 可能仅在声明文件被检查时能发现错误.
不过这种情况在实际使用中并不常见.

支持多个声明中出现重复的标示符

这是造成重复定义错误的常见原因之一.
多个声明文件在接口中定义了同样的成员.

TypeScript 2.0 放宽了相关约束并允许不同代码块中出现重复的标示符, 只要它们的类型等价.

在同一个代码块中的重复定义依然是不被允许的.

例子
  1. interface Error {
  2. stack?: string;
  3. }
  4. interface Error {
  5. code?: string;
  6. path?: string;
  7. stack?: string; // 正确
  8. }

新的 --declarationDir

--declarationDir 允许将声明文件生成到和 JavaScript 文件不同的位置.