Null 和 Undefined 感知类型
TypeScript 有两个特殊类型:Null 和 Undefined,它们分别具有值 null 和 undefined。以前无法显式命名这些类型,但现在无论类型检查模式如何,都可以将 null 和 undefined 用作类型名称。
类型检查器以前认为 null 和 undefined 可以赋值给任何类型。实际上,null 和 undefined 是每种类型的有效值,因此无法专门排除它们(也就无法检测到对它们的错误使用)。
--strictNullChecks
strictNullChecks 切换到一种新的严格空值检查模式。
在严格空值检查模式下,null 和 undefined 值不包含在每种类型的域中,它们只能赋值给自身和 any 类型(唯一的例外是 undefined 也可以赋值给 void)。因此,虽然在常规类型检查模式下 T 和 T | undefined 被视为同义(因为 undefined 被视为任何 T 的子类型),但在严格类型检查模式下它们是不同的类型,只有 T | undefined 允许使用 undefined 值。T 与 T | null 的关系也是如此。
示例
ts// Compiled with --strictNullCheckslet x: number;let y: number | undefined;let z: number | null | undefined;x = 1; // Oky = 1; // Okz = 1; // Okx = undefined; // Errory = undefined; // Okz = undefined; // Okx = null; // Errory = null; // Errorz = null; // Okx = y; // Errorx = z; // Errory = x; // Oky = z; // Errorz = x; // Okz = y; // Ok
使用前赋值检查
在严格空值检查模式下,编译器要求对不包含 undefined 的类型的局部变量的每次引用,必须在所有可能的先行代码路径中先进行赋值。
示例
ts// Compiled with --strictNullCheckslet x: number;let y: number | null;let z: number | undefined;x; // Error, reference not preceded by assignmenty; // Error, reference not preceded by assignmentz; // Okx = 1;y = null;x; // Oky; // Ok
编译器通过执行基于控制流的类型分析来检查变量是否已明确赋值。有关此主题的更多详细信息,请参阅后文。
可选参数和属性
可选参数和属性会自动将 undefined 添加到它们的类型中,即使它们的类型注解中没有显式包含 undefined。例如,以下两种类型是相同的
ts// Compiled with --strictNullCheckstype T1 = (x?: number) => string; // x has type number | undefinedtype T2 = (x?: number | undefined) => string; // x has type number | undefined
非空和非未定义类型守卫
如果对象或函数属于包含 null 或 undefined 的类型,则属性访问或函数调用会产生编译时错误。但是,类型守卫已扩展为支持非空和非未定义检查。
示例
ts// Compiled with --strictNullChecksdeclare function f(x: number): string;let x: number | null | undefined;if (x) {f(x); // Ok, type of x is number here} else {f(x); // Error, type of x is number? here}let a = x != null ? f(x) : ""; // Type of a is stringlet b = x && f(x); // Type of b is string | 0 | null | undefined
非空和非未定义类型守卫可以使用 ==、!=、=== 或 !== 运算符与 null 或 undefined 进行比较,例如 x != null 或 x === undefined。对主体变量类型的影响准确反映了 JavaScript 语义(例如,双等号运算符无论指定哪一个值都会检查两个值,而三等号仅检查指定的值)。
类型守卫中的点号名称
类型守卫以前仅支持检查局部变量和参数。现在类型守卫支持检查“点号名称”(dotted names),即由变量或参数名称后跟一个或多个属性访问组成。
示例
tsinterface Options {location?: {x?: number;y?: number;};}function foo(options?: Options) {if (options && options.location && options.location.x) {const x = options.location.x; // Type of x is number}}
点号名称的类型守卫也适用于用户定义的类型守卫函数,以及 typeof 和 instanceof 运算符,并且不依赖于 strictNullChecks 编译器选项。
对于点号名称的类型守卫,在对该点号名称的任何部分赋值后将失效。例如,对于 x.y.z 的类型守卫,在对 x、x.y 或 x.y.z 赋值后将不再起作用。
表达式运算符
表达式运算符允许操作数类型包含 null 和/或 undefined,但产生的值始终是非空和非未定义类型。
ts// Compiled with --strictNullChecksfunction sum(a: number | null, b: number | null) {return a + b; // Produces value of type number}
&& 运算符根据右操作数类型中是否存在 null 和/或 undefined 将其添加到结果类型中,而 || 运算符则从左操作数的类型中移除 null 和 undefined。
ts// Compiled with --strictNullChecksinterface Entity {name: string;}let x: Entity | null;let s = x && x.name; // s is of type string | nulllet y = x || { name: "test" }; // y is of type Entity
类型拓宽
在严格空值检查模式下,null 和 undefined 类型不会被拓宽为 any。
tslet z = null; // Type of z is null
在常规类型检查模式下,由于拓宽,z 的推断类型为 any;但在严格空值检查模式下,z 的推断类型为 null(因此,如果没有类型注解,null 是 z 的唯一可能值)。
非空断言运算符
可以使用新的 ! 后缀表达式运算符来断言其操作数在类型检查器无法得出该结论的上下文中是非空且非未定义的。具体而言,操作 x! 产生一个 x 类型的值,并排除 null 和 undefined。与 <T>x 和 x as T 形式的类型断言类似,! 非空断言运算符在编译生成的 JavaScript 代码中会被简单地删除。
ts// Compiled with --strictNullChecksfunction validateEntity(e?: Entity) {// Throw exception if e is null or invalid entity}function processEntity(e?: Entity) {validateEntity(e);let s = e!.name; // Assert that e is non-null and access name}
兼容性
新特性的设计使其既可以在严格空值检查模式下使用,也可以在常规类型检查模式下使用。特别地,在常规类型检查模式下,null 和 undefined 类型会自动从联合类型中移除(因为它们是所有其他类型的子类型),并且 ! 非空断言表达式运算符在常规类型检查模式下是允许使用的,但没有任何效果。因此,为了向后兼容,更新为使用 Null 和 Undefined 感知类型的声明文件仍然可以在常规类型检查模式下使用。
实际上,严格空值检查模式要求编译中的所有文件都必须是 Null 和 Undefined 感知的。
基于控制流的类型分析
TypeScript 2.0 实现了针对局部变量和参数的基于控制流的类型分析。以前,为类型守卫执行的类型分析仅限于 if 语句和 ?: 条件表达式,并不包括赋值和诸如 return 和 break 语句等控制流结构的影响。在 TypeScript 2.0 中,类型检查器会分析语句和表达式中所有可能的控制流,从而为声明为联合类型的局部变量或参数在任何给定位置生成最具体的类型(细化类型)。
示例
tsfunction foo(x: string | number | boolean) {if (typeof x === "string") {x; // type of x is string herex = 1;x; // type of x is number here}x; // type of x is number | boolean here}function bar(x: string | number) {if (typeof x === "number") {return;}x; // type of x is string here}
基于控制流的类型分析在 strictNullChecks 模式下尤为重要,因为可空类型是使用联合类型表示的。
tsfunction test(x: string | null) {if (x === null) {return;}x; // type of x is string in remainder of function}
此外,在 strictNullChecks 模式下,基于控制流的类型分析包括对不允许值 undefined 的类型的局部变量的明确赋值分析。
tsfunction mumble(check: boolean) {let x: number; // Type doesn't permit undefinedx; // Error, x is undefinedif (check) {x = 1;x; // Ok}x; // Error, x is possibly undefinedx = 2;x; // Ok}
标记联合类型(Tagged union types)
TypeScript 2.0 实现了对标记(或可辨识)联合类型的支持。具体而言,TS 编译器现在支持基于鉴别属性(discriminant property)测试来缩小联合类型的类型守卫,并将此功能扩展到了 switch 语句中。
示例
tsinterface Square {kind: "square";size: number;}interface Rectangle {kind: "rectangle";width: number;height: number;}interface Circle {kind: "circle";radius: number;}type Shape = Square | Rectangle | Circle;function area(s: Shape) {// In the following switch statement, the type of s is narrowed in each case clause// according to the value of the discriminant property, thus allowing the other properties// of that variant to be accessed without a type assertion.switch (s.kind) {case "square":return s.size * s.size;case "rectangle":return s.width * s.height;case "circle":return Math.PI * s.radius * s.radius;}}function test1(s: Shape) {if (s.kind === "square") {s; // Square} else {s; // Rectangle | Circle}}function test2(s: Shape) {if (s.kind === "square" || s.kind === "rectangle") {return;}s; // Circle}
鉴别属性类型守卫是形式为 x.p == v、x.p === v、x.p != v 或 x.p !== v 的表达式,其中 p 是一个属性,v 是字符串字面量类型或字符串字面量联合类型的表达式。鉴别属性类型守卫会将 x 的类型缩小为 x 中那些具有鉴别属性 p 且值为 v 可能值之一的组成类型。
请注意,目前我们仅支持字符串字面量类型的鉴别属性。我们打算稍后增加对布尔值和数字字面量类型的支持。
never 类型
TypeScript 2.0 引入了新的原始类型 never。never 类型表示从不出现的值的类型。具体而言,never 是从不返回的函数的返回类型,也是类型守卫中从不为真的变量的类型。
never 类型具有以下特征:
never是每种类型的子类型,并可赋值给每种类型。- 没有任何类型是
never的子类型或可赋值给never(除了never本身)。 - 在没有返回类型注解的函数表达式或箭头函数中,如果函数没有
return语句,或者只有never类型的表达式的return语句,并且函数的终点是不可达的(根据控制流分析确定),则函数的推断返回类型为never。 - 在具有显式
never返回类型注解的函数中,所有return语句(如果有)必须具有never类型的表达式,且函数的终点必须不可达。
由于 never 是每种类型的子类型,它始终会从联合类型中被省略,并且在存在其他返回类型的情况下,它在函数返回类型推断中会被忽略。
一些返回 never 的函数示例:
ts// Function returning never must have unreachable end pointfunction error(message: string): never {throw new Error(message);}// Inferred return type is neverfunction fail() {return error("Something failed");}// Function returning never must have unreachable end pointfunction infiniteLoop(): never {while (true) {}}
一些使用返回 never 的函数的示例:
ts// Inferred return type is numberfunction move1(direction: "up" | "down") {switch (direction) {case "up":return 1;case "down":return -1;}return error("Should never get here");}// Inferred return type is numberfunction move2(direction: "up" | "down") {return direction === "up"? 1: direction === "down"? -1: error("Should never get here");}// Inferred return type is Tfunction check<T>(x: T | undefined) {return x || error("Undefined value");}
由于 never 可以赋值给每种类型,因此当需要返回更具体类型的回调时,可以使用返回 never 的函数。
tsfunction test(cb: () => string) {let s = cb();return s;}test(() => "hello");test(() => fail());test(() => {throw new Error();});
只读属性和索引签名
现在可以使用 readonly 修饰符声明属性或索引签名。
只读属性可以有初始化器,也可以在同一类声明的构造函数中赋值,但除此之外禁止对只读属性赋值。
此外,实体在以下几种情况下是隐式只读的:
- 声明了
get访问器但没有set访问器的属性被视为只读。 - 在枚举对象的类型中,枚举成员被视为只读属性。
- 在模块对象的类型中,导出的
const变量被视为只读属性。 - 在
import语句中声明的实体被视为只读。 - 通过 ES2015 命名空间导入访问的实体被视为只读(例如,当
foo被声明为import * as foo from "foo"时,foo.x是只读的)。
示例
tsinterface Point {readonly x: number;readonly y: number;}var p1: Point = { x: 10, y: 20 };p1.x = 5; // Error, p1.x is read-onlyvar p2 = { x: 1, y: 1 };var p3: Point = p2; // Ok, read-only alias for p2p3.x = 5; // Error, p3.x is read-onlyp2.x = 5; // Ok, but also changes p3.x because of aliasing
tsclass Foo {readonly a = 1;readonly b: string;constructor() {this.b = "hello"; // Assignment permitted in constructor}}
tslet a: Array<number> = [0, 1, 2, 3, 4];let b: ReadonlyArray<number> = a;b[5] = 5; // Error, elements are read-onlyb.push(5); // Error, no push method (because it mutates array)b.length = 3; // Error, length is read-onlya = b; // Error, mutating methods are missing
为函数指定 this 的类型
继在类或接口中指定 this 的类型之后,函数和方法现在也可以声明它们期望的 this 类型。
默认情况下,函数内部 this 的类型是 any。从 TypeScript 2.0 开始,您可以提供一个显式的 this 参数。this 参数是假参数,位于函数参数列表的开头。
tsfunction f(this: void) {// make sure `this` is unusable in this standalone function}
回调函数中的 this 参数
库也可以使用 this 参数来声明回调函数将如何被调用。
示例
tsinterface UIElement {addClickListener(onclick: (this: void, e: Event) => void): void;}
this: void 表示 addClickListener 期望 onclick 是一个不需要 this 类型的函数。
现在,如果您使用 this 对调用代码进行注解
tsclass Handler {info: string;onClickBad(this: Handler, e: Event) {// oops, used this here. using this callback would crash at runtimethis.info = e.message;}}let h = new Handler();uiElement.addClickListener(h.onClickBad); // error!
--noImplicitThis
TypeScript 2.0 还添加了一个新标志,用于标记函数中所有未提供显式类型注解而使用 this 的情况。
tsconfig.json 中的 Glob 支持
Glob 支持来了!!Glob 支持是最受要求的功能之一。
Glob 风格的文件模式由 include 和 exclude 两个属性支持。
示例
{"": {"": "commonjs","": true,"": true,"": true,"": "../../built/local/tsc.js","": true},"": ["src/**/*"],"": ["node_modules", "**/*.spec.ts"]}
支持的 glob 通配符有:
*匹配零个或多个字符(不包括目录分隔符)?匹配任意单个字符(不包括目录分隔符)**/递归匹配任何子目录
如果 glob 模式的段仅包含 * 或 .*,则仅包含具有受支持扩展名的文件(例如,默认情况下包含 .ts、.tsx 和 .d.ts,如果将 allowJs 设置为 true,则还包含 .js 和 .jsx)。
如果未指定 files 和 include,编译器默认包含所在目录及子目录下所有 TypeScript(.ts、.d.ts 和 .tsx)文件,除非使用 exclude 属性排除。如果将 allowJs 设置为 true,也会包含 JS 文件(.js 和 .jsx)。
如果指定了 files 或 include 属性,编译器将改用这两个属性所包含文件的并集。除非通过 files 属性显式包含,否则目录中使用 outDir 编译器选项指定的文件始终会被排除(即使指定了 exclude 属性)。
使用 include 包含的文件可以使用 exclude 属性进行过滤。但是,使用 files 属性显式包含的文件无论是否排除都始终被包含。未指定时,exclude 属性默认排除 node_modules、bower_components 和 jspm_packages 目录。
模块解析增强:BaseUrl、路径映射、rootDirs 和跟踪
TypeScript 2.0 提供了一组额外的模块解析控制项,以告知编译器在何处查找给定模块的声明。
有关详细信息,请参阅模块解析文档。
Base URL
在运行期间模块被“部署”到单个文件夹的 AMD 模块加载器应用程序中,使用 baseUrl 是一种常见做法。所有具有裸说明符名称的模块导入都被假定为相对于 baseUrl。
示例
{"": {"": "./modules"}}
现在,对 "moduleA" 的导入将在 ./modules/moduleA 中查找。
tsimport A from "moduleA";
路径映射
有时模块并不直接位于 baseUrl 下。加载器使用映射配置在运行期间将模块名称映射到文件,请参阅 RequireJs 文档 和 SystemJS 文档。
TypeScript 编译器支持在 tsconfig.json 文件中使用 paths 属性声明此类映射。
示例
例如,对模块 "jquery" 的导入将在运行时转换为 "node_modules/jquery/dist/jquery.slim.min.js"。
{"": {"": "./node_modules","": {"jquery": ["jquery/dist/jquery.slim.min"]}}
使用 paths 还允许进行更复杂的映射,包括多个回退位置。考虑一种项目配置,其中某些模块在一个位置可用,而其余模块在另一个位置。
使用 rootDirs 的虚拟目录
使用 ‘rootDirs’,您可以告知编译器构成此“虚拟”目录的 根目录;这样编译器就可以解析这些“虚拟”目录内的相对模块导入,就好像它们被合并在一个目录中一样。
示例
假设有此项目结构:
src └── views └── view1.ts (imports './template1') └── view2.ts generated └── templates └── views └── template1.ts (imports './view2')
构建步骤会将 /src/views 和 /generated/templates/views 中的文件复制到输出中的同一目录。在运行期间,视图可以期望其模板与其相邻,因此应该使用相对名称 "./template" 来导入它。
rootDirs 指定了一组根目录,其内容预计会在运行时合并。因此,按照我们的示例,tsconfig.json 文件应如下所示:
{"": {"": ["src/views", "generated/templates/views"]}}
跟踪模块解析
traceResolution 提供了一种简单的方法来了解模块是如何被编译器解析的。
shelltsc --traceResolution
简写环境模块声明
如果您不想花时间在开始使用新模块之前编写声明,现在可以使用简写声明来快速入门。
declarations.d.ts
tsdeclare module "hot-new-module";
来自简写模块的所有导入都将具有 any 类型。
tsimport x, { y } from "hot-new-module";x(y);
模块名称中的通配符
使用模块加载器扩展(例如 AMD 或 SystemJS)导入非代码资源以前并不容易;此前必须为每个资源定义环境模块声明。
TypeScript 2.0 支持使用通配符 (*) 来声明“一系列”模块名称;这样,只需为扩展名进行一次声明,而不必为每个资源都进行声明。
示例
tsdeclare module "*!text" {const content: string;export default content;}// Some do it the other way around.declare module "json!*" {const value: any;export default value;}
现在,您可以导入匹配 "*!text" 或 "json!*" 的内容。
tsimport fileContent from "./xyz.txt!text";import data from "json!http://example.com/data.json";console.log(data, fileContent);
当从非类型化的代码库迁移时,通配符模块名称可能更有用。结合简写环境模块声明,可以轻松地将一组模块声明为 any。
示例
tsdeclare module "myLibrary/*";
对 myLibrary 下任何模块的所有导入都将被编译器视为具有 any 类型;从而关闭了对这些模块的形状或类型的任何检查。
tsimport { readFile } from "myLibrary/fileSystem/readFile`;readFile(); // readFile is 'any'
支持 UMD 模块定义
有些库被设计为用于许多模块加载器,或者不使用模块加载(全局变量)。这些被称为 UMD 或 Isomorphic 模块。这些库可以通过导入或全局变量访问。
例如:
math-lib.d.ts
tsexport const isPrime(x: number): boolean;export as namespace mathLib;
该库随后可以作为模块内的导入使用
tsimport { isPrime } from "math-lib";isPrime(2);mathLib.isPrime(2); // ERROR: can't use the global definition from inside a module
它也可以作为全局变量使用,但仅限于脚本内部。(脚本是没有任何导入或导出的文件。)
tsmathLib.isPrime(2);
可选类属性
现在可以在类中声明可选属性和方法,类似于接口中已允许的操作。
示例
tsclass Bar {a: number;b?: number;f() {return 1;}g?(): number; // Body of optional method can be omittedh?() {return 2;}}
当在 strictNullChecks 模式下编译时,可选属性和方法会自动包含 undefined 在其类型中。因此,上述 b 属性的类型为 number | undefined,上述 g 方法的类型为 (() => number) | undefined。类型守卫可用于去除类型的 undefined 部分。
tsfunction test(x: Bar) {x.a; // numberx.b; // number | undefinedx.f; // () => numberx.g; // (() => number) | undefinedlet f1 = x.f(); // numberlet g1 = x.g && x.g(); // number | undefinedlet g2 = x.g ? x.g() : 0; // number}
私有和受保护的构造函数
类构造函数可以标记为 private 或 protected。具有私有构造函数的类不能在类主体外部实例化,也不能被扩展。具有受保护构造函数的类不能在类主体外部实例化,但可以被扩展。
示例
tsclass Singleton {private static instance: Singleton;private constructor() {}static getInstance() {if (!Singleton.instance) {Singleton.instance = new Singleton();}return Singleton.instance;}}let e = new Singleton(); // Error: constructor of 'Singleton' is private.let v = Singleton.getInstance();
抽象属性和访问器
抽象类可以声明抽象属性和/或访问器。任何子类都需要声明这些抽象属性或被标记为抽象。抽象属性不能有初始化器。抽象访问器不能有主体。
示例
tsabstract class Base {abstract name: string;abstract get value();abstract set value(v: number);}class Derived extends Base {name = "derived";value = 1;}
隐式索引签名
如果对象字面量中的所有已知属性都可以赋值给索引签名,则该对象字面量类型现在可以赋值给具有该索引签名的类型。这使得可以将初始化为对象字面量的变量作为参数传递给期望映射或字典的函数。
tsfunction httpService(path: string, headers: { [x: string]: string }) {}const headers = {"Content-Type": "application/x-www-form-urlencoded",};httpService("", { "Content-Type": "application/x-www-form-urlencoded" }); // OkhttpService("", headers); // Now ok, previously wasn't
使用 --lib 包含内置类型声明
获取 ES6/ES2015 内置 API 声明以前仅限于 target: ES6。现在引入了 lib;通过 lib,您可以指定一组要包含在项目中的内置 API 声明组。例如,如果您期望运行环境支持 Map、Set 和 Promise(例如当今大多数现代浏览器),只需包含 --lib es2015.collection,es2015.promise。同样,您可以排除不想包含在项目中的声明,例如,如果您在节点项目中使用 --lib es5,es6,则可以排除 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
示例
bashtsc --target es5 --lib es5,es2015.promise
"compilerOptions": {"": ["es5", "es2015.promise"]}
使用 --noUnusedParameters 和 --noUnusedLocals 标记未使用的声明
TypeScript 2.0 有两个新标志,可帮助您维护干净的代码库。noUnusedParameters 会将任何未使用的函数或方法参数标记为错误。noUnusedLocals 会将任何未使用的(非导出)局部声明(如变量、函数、类、导入等)标记为错误。此外,类中未使用的私有成员在 noUnusedLocals 下也会被标记为错误。
示例
tsimport B, { readFile } from "./b";// ^ Error: `B` declared but never usedreadFile();export function write(message: string, args: string[]) {// ^^^^ Error: 'arg' declared but never used.console.log(message);}
以 _ 开头命名的参数声明不受未使用参数检查的影响。例如:
tsfunction returnNull(_a) {// OKreturn null;}
模块标识符允许 .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 : es5’ 与 ‘module: es6’
以前被标记为无效的标志组合,现在支持 target: es5 和 module: es6。这应该有助于使用基于 ES2015 的 tree-shaker,如 rollup。
函数参数和实参列表中的尾随逗号
现在允许在函数参数和实参列表中使用尾随逗号。这是对 Stage-3 ECMAScript 提案 的实现,该提案可以向下编译为有效的 ES3/ES5/ES6。
示例
tsfunction foo(bar: Bar,baz: Baz // trailing commas are OK in parameter lists) {// Implementation...}foo(bar,baz // and in argument lists);
新的 --skipLibCheck
TypeScript 2.0 添加了一个新的 skipLibCheck 编译器选项,该选项会跳过声明文件(扩展名为 .d.ts 的文件)的类型检查。当程序包含大型声明文件时,编译器会花费大量时间检查已知不包含错误的声明,跳过声明文件的类型检查可以显著缩短编译时间。
由于一个文件中的声明可能会影响其他文件中的类型检查,因此指定 skipLibCheck 时可能无法检测到某些错误。例如,如果非声明文件扩充了声明文件中声明的类型,则可能会产生仅在检查该声明文件时才报告的错误。然而,在实践中,这种情况很少见。
允许跨声明的重复标识符
这是重复定义错误的一个常见来源。多个声明文件在接口上定义相同的成员。
TypeScript 2.0 放宽了这一限制,只要类型相同,就允许跨块出现重复标识符。
在同一块内,仍然不允许重复定义。
示例
tsinterface Error {stack?: string;}interface Error {code?: string;path?: string;stack?: string; // OK}
新的 --declarationDir
declarationDir 允许在与 JavaScript 文件不同的位置生成声明文件。