一.语法格式 TypeScript 兼容 ES Module 规范,文件即模块

简单来讲,如果一个文件中含有合法的import或export语句,就会被当做模块(拥有模块作用域),否则就将在运行在全局作用域下。例如:


let x = 1
function f() { }
// 会被编译成
var x = 1;
function f() { }

// 而
let x = 1
export function f() { }
// 会被编译成(以 AMD 形式为例)
define(["require", "exports"], function (require, exports) {
  "use strict";
  Object.defineProperty(exports, "__esModule", { value: true });
  var x = 1;
  function f() { }
  exports.f = f;
});

任何声明都能被import/export,包括接口、类型别名等等:


export interface StringValidator {
  isAcceptable(s: string): boolean;
}
export type PhoneNumber = string;

特殊的,纯声明文件(如d.ts)虽然不会生成有实际意义的代码,但仍具有模块(作用域)隔离:


// 上例会被编译成
define(["require", "exports"], function (require, exports) {
  "use strict";
  Object.defineProperty(exports, "__esModule", { value: true });
});

这也是d.ts 分类的依据之一

P.S.import/export具体语法见ES Module,这里不展开

CommonJS 模块支持 为了支持CommonJS 和 AMD 模块,TypeScript 提供了一种特殊语法:

export = something; 用来定义一个模块的导出对象,类似于 NodeJS 里的:


// NodeJS模块(CommonJS)
let x = {a: 1};
exports.x = x;
module.exports = x;

改写成 TypeScript 的话是这样:


let x = {a: 1};
export = x;
// 会被编译成
define(["require", "exports"], function (require, exports) {
  "use strict";
  var x = { a: 1 };
  return x;
});

对应的引入语法也不同于 NodeJS(require('./myModule.js')):


import module = require("myModule")

二.模块代码生成 可以通过编译选项(--module或-m)来指定生成代码的模块格式:


  // tsc -m xxx
 'commonjs' # NodeJS模块定义
 'amd'      # AMD
 'system'   # SystemJS
 'umd'      # UMD
 'es6'      # ES Module
 'es2015'   # 等价于es6
 
 

'esnext' # 尚未收入ES规范的前沿模块定义,如import(), import.meta等 'none' # 禁用所有模块定义,如import, export等(用到的话会报错) 默认模块格式为 CommonJS 或 ES6,与--target选项有关(target === "ES3" or "ES5" ? "CommonJS" : "ES6")。如果将来新版本 ES 规范中模块定义有改动的话,还会新增es2019, es2020...等值,对应 ES 规范各个版本中的模块定义(如果模块定义没有改动的话,就不加)

P.S.具体的模块生成示例,见Code Generation for Modules

--module与--target --target(或-t)选项与--module很像,取值如下:


// tsc -t xxx
'es3'
'es5'
'es2015'
'es2016'
'es2017'
'es2018'
'esnext'

表示生成的目标代码支持哪一版规范所定义的语言特性(默认 ES3),与--module选项是独立的:


The module system is independent of the language implementation.

因为完全可以编译生成满足 ES6 模块格式的 ES5 代码,例如:


// tsconfig.json
"compilerOptions": {
  "target": "es5",
  "module": "es6"
}

另外,取值上也不同于--module,每一版 ES 规范都会对应一个--target具体值,因为每一版都会有新的特性加入

P.S.更多相关讨论,见Understanding “target” and “module” in tsconfig

P.S.注意,--module和--target都是针对将要生成的目标代码的,与源码无关(源码完全可以 ES 特性全开,如--lib指定ES2016, es2017...)

三.模块引入 一般情况下,import/require会引入目标模块源码,并从中提取类型信息,例如:

// myModule.ts
export default {
  name: 'my-module',
  f() {
    console.log('this is my module.');
  }
}

// index.ts
import MyModule from './MyModule';
let m = MyModule;
// m 的类型为 { name: string; f(): void; }
m.f();

(--module commonjs下)index.ts编译结果为:


exports.__esModule = true;
var MyModule_1 = require("./MyModule");
var m = MyModule_1["default"];
// m 的类型为 { name: string; f(): void; }
m.f();

按需加载 特殊的,如果生成的目标代码中没有用到被引入的模块(如仅在类型标注中使用),编译时会自动去掉模块引用:


// index.ts
import MyModule from './MyModule';
let m: typeof MyModule;

// 编译结果
exports.__esModule = true;
var m;

这种去除非必要引用(reference-elision)的特性在按需加载的场景尤为重要:


// 引入类型
import MyModule from './MyModule';
declare function require(moduleName: string): any;
let someCondition: boolean;
if (someCondition) {
  let m: typeof MyModule = require('./MyModule');
  // 同样具有正确的类型
  m.f();
}

// 编译结果
"use strict";
exports.__esModule = true;
var someCondition;
if (someCondition) {
  var m = require('./MyModule');
  // 同样具有正确的类型
  m.f();
}

四.模块类型声明 对于缺少类型的第三方模块,可以通过声明文件(d.ts)为其补充类型声明

具体的,declare module 'my-module' {}语法能够声明一个模块(能被import/require):


// types.d.ts
declare module "my-module" {
  function f(): string;
}

// index.ts
import { f } from "my-module";
const result: string = f();

可以通过这种方式来填补第三方模块的类型,但如果只是想快速使用(不愿意手动补类型)的话,可以省略成员声明,其所有成员都将是any类型:


// types.d.ts
declare module "my-module";

// index.ts
import x, {y} from "my-module";
x(y);

通配符 特殊的,某些加载系统支持引入非 JavaScript 内容,例如AMD:


define(['text!../templates/start.html'], function (template) {
    //do something with the template text string.
});

此时可以通过模块通配符来定义其类型:


// 描述所有以 text! 开头的模块的类型
declare module "text!*" {
  const content: string;
  export default content;
}
// 描述所有以 !text 结尾的模块的类型
declare module "*!text" {
  const content: string;
  export default content;
}

这些特殊的模块就具有类型信息了:


import html from 'text!../templates/start.html';
// 正确
html.trim();

UMD 模块 UMD的特点是既兼容 CommonJS 和 AMD 模块加载,也可以暴露到全局直接使用,因此其模块声明也比较特殊:


// math-lib.d.ts
export function isPrime(x: number): boolean;
export as namespace mathLib;

两种引用方式:


// 直接通过全局变量访问
mathLib.isPrime(12);
// 模块引入
import { isPrime } from './math-lib';
isPrime(122);