typescript 中的module语法详解

1,141 阅读5分钟

在 TypeScript 中,就像在 ECMAScript 2015 中一样,任何包含 import 或 export 的文件都被视为模块;相反,没有 import 或 export 的文件被视为其内容在全局范围内可用的脚本。

Non-modules

如果文件中没有 import 或 export 那么它将作为一个js脚本zhon 在Js脚本文件中,变量和类型被声明为在共享全局范围内; 如果有一个不需要导出和导入的文件,但希望ts将其按一个模块处理,可以添加:

export {}
//  build
// initWindowVar.ts
// set REGION and ENVIRONMENT
// private block用来区分地区和环境
window.REGION = process.env.SHOPEE_COUNTRY;
window.ENVIRONMENT = process.env.SHOPEE_ENVIRONMENT;
// private block用来判断是否是在admin builder
window.IS_IN_BUILDER = true;

export {};

使用outFile可以将多个脚本合成一个文件,但 module  只能设置为None, System, 和 AMD

Exporting a declaration

可以用 export 导出 变量,函数,接口,类型别名等。

// StringValidator.ts
export interface StringValidator {
  isAcceptable(s: string): boolean;
}
import { StringValidator } from "./StringValidator";
export const numberRegexp = /^[0-9]+$/;
export class ZipCodeValidator implements StringValidator {
  isAcceptable(s: string) {
    return s.length === 5 && numberRegexp.test(s);
  }
}
// 重命名
export { ZipCodeValidator as mainValidator };

Re-exports

一个module可以合并导出其他的modules所有导出的变量,方法等。 export * from "module"

// ParseIntBasedZipCodeValidator.ts
export class ParseIntBasedZipCodeValidator {
  isAcceptable(s: string) {
    return s.length === 5 && parseInt(s).toString() === s;
  }
}
// Export original validator but rename it
export { ZipCodeValidator as RegExpBasedZipCodeValidator } from "./ZipCodeValidator";
export * from "./StringValidator"; // exports 'StringValidator' interface
export * from "./ZipCodeValidator"; // exports 'ZipCodeValidator' class and 'numberRegexp' constant value
export * from "./ParseIntBasedZipCodeValidator"; //  exports the 'ParseIntBasedZipCodeValidator' class
// and re-exports 'RegExpBasedZipCodeValidator' as alias
// of the 'ZipCodeValidator' class from 'ZipCodeValidator.ts'
// module.

Import Syntax

可用通过 import 来引用其他模块导出的变量,方法等。

import { ZipCodeValidator } from "./ZipCodeValidator";
let myValidator = new ZipCodeValidator();

// 重命名
import { ZipCodeValidator as ZCV } from "./ZipCodeValidator";
let myValidator = new ZCV();

import as Syntax

import * as validator from "./ZipCodeValidator";
let myValidator = new validator.ZipCodeValidator();

Import a module for side-effects

模块没有任何导出,或者使用方并不关心模块的导出,只是需要引入一些全局状态

// builder
// 在window上挂载变量
import './initWindowVar';

import default

export default class ZipCodeValidator {
  static numberRegexp = /^[0-9]+$/;
  isAcceptable(s: string) {
    return s.length === 5 && ZipCodeValidator.numberRegexp.test(s);
  }
}

import validator from "./ZipCodeValidator";
// JQuery.d.ts
declare let $: JQuery;
export default $;

// index.ts
import $ from "jquery";
$("button.continue").html("Next Step...");

Importing Types

typescript 的模块导入导出,除了实现es标准模块的导入导出之后,还实现有它自身基于类型的导入导出,这属于编译阶段的导入导出,在编译完成之后,这个类型的导入导出语句是需要擦除的。

// utils
export const add = (a, b) => {
  return a + b;
}
export interface BaseResponse<T = unknown> {
  code: number;
  data: T;
  msg: string;
}

// index.ts
import { add, BaseResponse } from './utils';
const res: BaseResponse<string[]> = {
  code: 0,
  data: [],
  msg: 'success'
}
console.log(add(1,3));
console.log(res);

编译后

// util.js
export const add = (a, b) => {
    return a + b;
};

// index.js
import { add } from './utils';
const res = {
    code: 0,
    data: [],
    msg: 'success'
};
console.log(add(1, 3));
console.log(res);

这种处理方式,默认情况下是没问题的,因为ts能够识别哪些导入导出是类型,然后在导出结果中自动擦除。

但是有一些场景中,这个机制有点问题,所以typescript 3.8 新增了 import type语法 来帮助我们指定导入导出的是类型还是值; 详细可查看 type-only-imports-exports

具体场景

// foo.ts
export interface Options {
  label: string;
  value: string;
}
// bar.ts
import { Options } from "./foo";
// 单看bar文件 无法分辨 option到底是类型 还是value ?
export {Options};

默认情况下,typescript会在项目范围内分析 模块的内容,上述情况下 我们用tsc可以正常编译。

编译后

// foo.js
export {};
// bar.js
export {};

但像babel这样的工具,一次这能单个文件;当然,如果配置了isolatedModules tsc 也只会编译单文件。

alt isolatedModules_1

并且开启了isolatedModules后,项目的文件必须按模块来处理,但不包括.d.ts

在 TypeScript 中,当引用 const 枚举成员时,编译为js文件,其引用会被替换为实际值。

// d.ts
declare const enum Numbers {
  Zero = 0,
  One = 1,
}

编译后:

console.log(0 /* Numbers.Zero */ + 1 /* Numbers.One */);

 像babel这类编译器,只能单个文件处理,所以找不到对应的引用,然后就会导致运行时的错误。

babel编译结果

console.log(Numbers.Zero + Numbers.One);
// 运行报错

通过import type导入的类型在使用时只能当成type使用,不能当成value使用

import type { Base } from "my-library";
import type Foo, { Bar, Baz } from "some-module";
//     ~~~~~~~~~~~~~~~~~~~~~~
// error! A type-only import can specify a default import or named bindings, but not both.

let baseConstructor: typeof Base;
//                          ~~~~
// error! 'Base' only refers to a type, but is being used as a value here.

declare class Derived extends Base {
    //                        ~~~~
    // error! 'Base' only refers to a type, but is being used as a value here.
}

// animal.ts
export class Animal {
    name: string
}

// comsumer.ts
import type {Animal} from "./animal"

let animal = new Animal()
//               ~~~~~~
// 'Animal' cannot be used as a value because it was imported using 'import type'.

export = and import = require()

ts中也可支持commonJs规范, 需要将tsconfig中的 module设为CommonJS

// ZipCodeValidator.ts
let numberRegexp = /^[0-9]+$/;
class ZipCodeValidator {
  isAcceptable(s: string) {
    return s.length === 5 && numberRegexp.test(s);
  }
}
export = ZipCodeValidator;
// index.ts
import zip = require("./ZipCodeValidator");
// Some samples to try
let strings = ["Hello", "98052", "101"];
// Validators to use
let validator = new zip();
// Show whether each string passed each validator
strings.forEach((s) => {
  console.log(
    `"${s}" - ${validator.isAcceptable(s) ? "matches" : "does not match"}`
  );
});

可以设置 esModuleInterop: true 就可以直接使用普通的import语法

esModuleInterop

import zip from './ZipCodeValidator';
// Some samples to try
let strings = ["Hello", "98052", "101"];
// Validators to use
let validator = new zip();
// Show whether each string passed each validator
strings.forEach((s) => {
  console.log(
    `"${s}" - ${validator.isAcceptable(s) ? "matches" : "does not match"}`
  );
});

Module Output Options

typescript 支持的module输出格式 如下

alt module format

Module Optional Loading

有些模块并不是页面初始就要执行,所以为了提升页面的加载速度,我们需要对这样的模块做到按需加载。 可以采用 import id = require("...")语法来完成模块的按需加载

// ZipCodeValidator.ts
export interface StringValidator {
  isAcceptable(s: string): boolean;
}
const numberRegexp = /^[0-9]+$/;
export class ZipCodeValidator implements StringValidator {
  isAcceptable(s: string) {
    return s.length === 5 && numberRegexp.test(s);
  }
}

// index.ts
declare function require(moduleName: string): any;
import { ZipCodeValidator as Zip } from "./ZipCodeValidator";

const needZipValidation = false;

if (needZipValidation) {
  // 确保类型类型安全,必须带上typeof
  let ZipCodeValidator: typeof Zip = require("./ZipCodeValidator");
  let validator = new ZipCodeValidator();
  if (validator.isAcceptable("...")) {
    /* ... */
  }
}

编译后

// ZipCodeValidator.js
const numberRegexp = /^[0-9]+$/;
export class ZipCodeValidator {
    isAcceptable(s) {
        return s.length === 5 && numberRegexp.test(s);
    }
}

// index.js
const needZipValidation = false;
if (needZipValidation) {
    let ZipCodeValidator = require("./ZipCodeValidator");
    let validator = new ZipCodeValidator();
    if (validator.isAcceptable("...")) {
        /* ... */
    }
}
export {};

Re-export to extend

当我们需要对模块内容进行扩展时,一种常见的js解决方案是 mixin。但是typescript不推荐修改原来的模块,可以通过像继承这样的方式来维护一个新的模块。

export class Calculator{
  evaluate() {
    console.log('-Calculator --')
  } 
}

export class NewCalculator extends Calculator {
  evaluate() {
    console.log('-NewCalculator --')
  }
}

Module Resolution

typescript 有两种模块查找的策略:ClassicNode. 如果按以上两种策略找不到模块,则会根据项目的类型声明文件来查找模块。 具体可查看ambient module declaration

可以在.d.ts文件,声明模块

可通过moduleResolution 来配置ts的模块解析策略。

declare module "path" {
  export function normalize(p: string): string;
  export function join(...paths: any[]): string;
  export var sep: string;
}

declare module '*.png' {
  const src: string;
  export default src;
}

declare module '*.module.less' {
  const classes: { [key: string]: string };
  export default classes;
}
//  umd可以导出一个全局命名空间
export function isPrime(x: number): boolean;
export as namespace mathLib;

最后,如果编译器无法解析模块,它抛出错误。

Classic

typescript 默认的解析策略

相对路径

import { b } from "./moduleB";
// 相对当前文件来查找modulesB, 假设当前文件路径是 '/root/src/folder/A.ts'

// 按以下路径查找
// root/src/folder/moduleB.ts
// root/src/folder/moduleB.d.ts

非相对路径

import { b } from "moduleB";

// 查找范围
/root/src/folder/moduleB.ts
/root/src/folder/moduleB.d.ts
/root/src/moduleB.ts
/root/src/moduleB.d.ts
/root/moduleB.ts
/root/moduleB.d.ts
/moduleB.ts
/moduleB.d.ts

Node

TypeScript 将模仿 Node.js 运行时解析策略,以便在编译时定位模块的定义文件。 为此,TypeScript 将 TypeScript 源文件扩展名(.ts、.tsx 和 .d.ts)然后按照 Node 的解析逻辑来查找模块。 TypeScript 还将使用 package.json 中名为 types 的字段来代替 ‘main’字段的作用。

相对路径

import { b } from "./moduleB"
// /root/src/moduleA.ts

// 查找范围
/root/src/moduleB.ts
/root/src/moduleB.tsx
/root/src/moduleB.d.ts
/root/src/moduleB/package.json (if it specifies a types property)
/root/src/moduleB/index.ts
/root/src/moduleB/index.tsx
/root/src/moduleB/index.d.ts

非相对路径

import { b } from "moduleB"
// source file: /root/src/moduleA.ts

// 查找范围
/root/src/node_modules/moduleB.ts
/root/src/node_modules/moduleB.tsx
/root/src/node_modules/moduleB.d.ts
/root/src/node_modules/moduleB/package.json (if it specifies a types property)
/root/src/node_modules/@types/moduleB.d.ts
/root/src/node_modules/moduleB/index.ts
/root/src/node_modules/moduleB/index.tsx
/root/src/node_modules/moduleB/index.d.ts

/root/node_modules/moduleB.ts
/root/node_modules/moduleB.tsx
/root/node_modules/moduleB.d.ts
/root/node_modules/moduleB/package.json (if it specifies a types property)
/root/node_modules/@types/moduleB.d.ts
/root/node_modules/moduleB/index.ts
/root/node_modules/moduleB/index.tsx
/root/node_modules/moduleB/index.d.ts

/node_modules/moduleB.ts
/node_modules/moduleB.tsx
/node_modules/moduleB.d.ts
/node_modules/moduleB/package.json (if it specifies a types property)
/node_modules/@types/moduleB.d.ts
/node_modules/moduleB/index.ts
/node_modules/moduleB/index.tsx
/node_modules/moduleB/index.d.ts

Tracing module resolution

可以使用 tsc --traceResolution 来跟踪模块的解析过程。

BaseUrl

可以使用baseUrl 来定义模块的起始查找路径

未定义baseUrl

// 查找过程

/module-in-ts/src/module-reslution/index.ts
/module-in-ts/src/module-reslution/node_modules
/module-in-ts/src/node_modules
/module-in-ts/node_modules/axios/package.json
/module-in-ts/node_modules/axios.ts (.tsx , .d.ts)
...
/module-in-ts/node_modules/axios/index.d.ts

定义baseUrl 在tsconfig.json定义 baseUrl: './'

// 查找过程

/module-in-ts/axios.ts (.tsx, .d.ts)
/module-in-ts/node_modules/axios/package.json
/module-in-ts/node_modules/axios.ts (.tsx , .d.ts)
...
/module-in-ts/node_modules/axios/index.d.ts

path

可通过path配置模块路径映射

 "paths": {
   "@/*": [
      "src/*"
    ]
 }

path 是相对于baseUrl的

Virtual Directories

如果不同层级的目录,最后打包时会打到同级目录。可以考虑使用虚拟目录来通过ts的编译 www.typescriptlang.org/docs/handbo…