TypeScript 学习指南 Part 4-8 Handbook Modules

147 阅读4分钟

Modules

模块化编程

JavaScript has a long history of different ways to handle modularizing code. TypeScript having been around since 2012, has implemented support for a lot of these formats, but over time the community and the JavaScript specification has converged on(收敛于) a format called ES Modules (or ES6 modules). You might know it as the import/export syntax.

ES Modules was added to the JavaScript spec in 2015, and by 2020 had broad support in most web browsers and JavaScript runtimes.

For focus, the handbook will cover both ES Modules and its popular pre-cursor CommonJS module.exports = syntax, and you can find information about the other module patterns in the reference section under Modules.

How JavaScript Modules are Defined

In TypeScript, just as in ECMAScript 2015, any file containing a top-level import or export is considered a module.

Conversely(相反地), a file without any top-level import or export declarations is treated as a script whose contents are available in the global scope (and therefore to modules as well).

Modules are executed within their own scope, not in the global scope. This means that variables, functions, classes, etc.[ˌet ˈsetərə] declared in a module are not visible outside the module unless they are explicitly exported using one of the export forms. Conversely(相反地), to consume a variable, function, class, interface, etc. exported from a different module, it has to be imported using one of the import forms.

Non-modules

Before we start, it’s important to understand what TypeScript considers a module. The JavaScript specification declares that any JavaScript files without an export or top-level await should be considered a script and not a module.

Inside a script file variables and types are declared to be in the shared global scope, and it’s assumed that you’ll either use the outFile compiler option to join multiple input files into one output file, or use multiple <script> tags in your HTML to load these files (in the correct order!).

If you have a file that doesn’t currently have any imports or exports, but you want to be treated as a module, add the line:

export {};
Try

which will change the file to be a module exporting nothing. This syntax works regardless of your module target.

Modules in TypeScript

Additional Reading:
Impatient JS (Modules)
MDN: JavaScript Modules\

There are three main things to consider when writing module-based code in TypeScript:

  • Syntax: What syntax do I want to use to import and export things?
  • Module Resolution: What is the relationship between module names (or paths) and files on disk?
  • Module Output Target: What should my emitted JavaScript module look like?

ES Module Syntax

A file can declare a main export via export default:

// @filename: hello.ts
export default function helloWorld() {
  console.log("Hello, world!");
}
Try

This is then imported via:

import helloWorld from "./hello.js";
helloWorld();
Try

In addition to the default export, you can have more than one export of variables and functions via the export by omitting (省略) default:

// @filename: maths.ts
export var pi = 3.14;
export let squareTwo = 1.41;
export const phi = 1.61;
 
export class RandomNumberGenerator {}
 
export function absolute(num: number) {
  if (num < 0) return num * -1;
  return num;
}
Try

These can be used in another file via the import syntax:

import { pi, phi, absolute } from "./maths.js";
 
console.log(pi);
const absPhi = absolute(phi);
        
const absPhi: number
Try

Additional Import Syntax

An import can be renamed using a format like import {old as new}:

import { pi as π } from "./maths.js";
 
console.log(π);
           
(alias) var π: number
import π
Try

You can mix and match the above syntax into a single import:

// @filename: maths.ts
export const pi = 3.14;
export default class RandomNumberGenerator {}
 
// @filename: app.ts
import RandomNumberGenerator, { pi as π } from "./maths.js";
 
RandomNumberGenerator;
         
(alias) class RandomNumberGenerator
import RandomNumberGenerator
 
console.log(π);
           
(alias) const π: 3.14
import π
Try

You can take all of the exported objects and put them into a single namespace using * as name:

// @filename: app.ts
import * as math from "./maths.js";
 
console.log(math.pi);
const positivePhi = math.absolute(math.phi);
          
const positivePhi: number
Try

You can import a file and not include any variables into your current module via import "./file":

// @filename: app.ts
import "./maths.js";
 
console.log("3.14");
Try

In this case, the import does nothing. However, all of the code in maths.ts was evaluated, which could trigger side-effects which affect other objects.

TypeScript Specific ES Module Syntax

Types can be exported and imported using the same syntax as JavaScript values:

// @filename: animal.ts
export type Cat = { breed: string; yearOfBirth: number };
 
export interface Dog {
  breeds: string[];
  yearOfBirth: number;
}
 
// @filename: app.ts
import { Cat, Dog } from "./animal.js";
type Animals = Cat | Dog;
Try

TypeScript has extended the import syntax with two concepts for declaring an import of a type:

import type

Which is an import statement which can only import types:

// @filename: animal.ts
export type Cat = { breed: string; yearOfBirth: number };
'createCatName' cannot be used as a value because it was imported using 'import type'.'createCatName' cannot be used as a value because it was imported using 'import type'.
export type Dog = { breeds: string[]; yearOfBirth: number };
export const createCatName = () => "fluffy";
 
// @filename: valid.ts
import type { Cat, Dog } from "./animal.js";
export type Animals = Cat | Dog;
 
// @filename: app.ts
import type { createCatName } from "./animal.js";
const name = createCatName();
Try
Inline type imports

TypeScript 4.5 also allows for individual imports to be prefixed with type to indicate that the imported reference is a type:

// @filename: app.ts
import { createCatName, type Cat, type Dog } from "./animal.js";
 
export type Animals = Cat | Dog;
const name = createCatName();
Try

Together these allow a non-TypeScript transpiler like Babel, swc or esbuild to know what imports can be safely removed.

ES Module Syntax with CommonJS Behavior

TypeScript has ES Module syntax which directly correlates to (相关) a CommonJS and AMD require. Imports using ES Module are for most cases the same as the require from those environments, but this syntax ensures you have a 1 to 1 match in your TypeScript file with the CommonJS output:

import fs = require("fs");
const code = fs.readFileSync("hello.ts", "utf8");
Try

You can learn more about this syntax in the modules reference page.

CommonJS Syntax

CommonJS is the format which most modules on npm are delivered in. Even if you are writing using the ES Modules syntax above, having a brief understanding of how CommonJS syntax works will help you debug easier.

CommonJS是npm上大多数模块的交付格式。即使您正在使用上述ES模块语法进行编写,简要了解CommonJS语法的工作原理也将帮助您更轻松地进行调试。

Exporting

Identifiers are exported via setting the exports property on a global called module.

function absolute(num: number) {
  if (num < 0) return num * -1;
  return num;
}
 
module.exports = {
  pi: 3.14,
  squareTwo: 1.41,
  phi: 1.61,
  absolute,
};
Try

Then these files can be imported via a require statement:

const maths = require("maths");
maths.pi;
      
any
Try

Or you can simplify a bit using the destructuring feature in JavaScript:

const { squareTwo } = require("maths");
squareTwo;
   
const squareTwo: any
Try

CommonJS and ES Modules interop 互操作

There is a mis-match in features between CommonJS and ES Modules regarding the distinction between a default import and a module namespace object import. TypeScript has a compiler flag to reduce the friction between the two different sets of constraints with esModuleInterop.

关于默认导入和模块命名空间对象导入之间的区别,CommonJS和ES模块之间的功能不匹配。TypeScript有一个编译器标志,用于使用esModuleInterop减少两组不同约束之间的摩擦。

TypeScript’s Module Resolution Options

Module resolution is the process of taking a string from the import or require statement, and determining what file that string refers to.

TypeScript includes two resolution strategies: Classic and Node. Classic, the default when the compiler option module is not commonjs, is included for backwards compatibility. The Node strategy replicates how Node.js works in CommonJS mode, with additional checks for .ts and .d.ts.

There are many TSConfig flags which influence the module strategy within TypeScript: moduleResolutionbaseUrlpathsrootDirs.

For the full details on how these strategies work, you can consult the Module Resolution.

TypeScript’s Module Output Options

There are two options which affect the emitted JavaScript output:

  • target which determines which JS features are downleveled (converted to run in older JavaScript runtimes) and which are left intact
  • module which determines what code is used for modules to interact with each other

Which target you use is determined by the features available in the JavaScript runtime you expect to run the TypeScript code in. That could be: the oldest web browser you support, the lowest version of Node.js you expect to run on or could come from unique constraints from your runtime - like Electron for example.

All communication between modules happens via a module loader, the compiler option module determines which one is used. At runtime the module loader is responsible for locating and executing all dependencies of a module before executing it.

For example, here is a TypeScript file using ES Modules syntax, showcasing a few different options for module:

import { valueOfPi } from "./constants.js";
 
export const twoPi = valueOfPi * 2;
Try

ES2020

import { valueOfPi } from "./constants.js";
export const twoPi = valueOfPi * 2;
 
Try

CommonJS

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.twoPi = void 0;
const constants_js_1 = require("./constants.js");
exports.twoPi = constants_js_1.valueOfPi * 2;
 
Try

UMD

(function (factory) {
    if (typeof module === "object" && typeof module.exports === "object") {
        var v = factory(require, exports);
        if (v !== undefined) module.exports = v;
    }
    else if (typeof define === "function" && define.amd) {
        define(["require", "exports", "./constants.js"], factory);
    }
})(function (require, exports) {
    "use strict";
    Object.defineProperty(exports, "__esModule", { value: true });
    exports.twoPi = void 0;
    const constants_js_1 = require("./constants.js");
    exports.twoPi = constants_js_1.valueOfPi * 2;
});
 
Try

Note that ES2020 is effectively the same as the original index.ts.

You can see all of the available options and what their emitted JavaScript code looks like in the TSConfig Reference for module.

TypeScript namespaces

TypeScript has its own module format called namespaces which pre-dates (早于;先于…建成(或形成、发生等)) the ES Modules standard. This syntax has a lot of useful features for creating complex definition files, and still sees active use in DefinitelyTyped. While not deprecated, the majority of the features in namespaces exist in ES Modules and we recommend you use that to align with JavaScript’s direction. You can learn more about namespaces in the namespaces reference page.

TypeScript有自己的模块格式,称为命名空间namespaces,早于ES模块标准。这种语法有许多用于创建复杂定义文件的有用特性,在in DefinitelyTyped中仍然可以使用。虽然没有被弃用,但名称空间namespaces中的大多数功能都存在于ES模块中,我们建议您使用它来与JavaScript的方向保持一致。您可以在名称空间参考页面中了解有关名称空间the namespaces reference page的更多信息。

参考文章

segmentfault.com/a/119000004…