TypeScript 之模块

TypeScript 的官方文档早已更新,但我能找到的中文文档都还停留在比较老的版本。所以对其中新增以及修订较多的一些章节进行了翻译整理。

本篇翻译整理自 TypeScript Handbook 中 「Module」 章节。

本文并不严格按照原文翻译,对部分内容也做了解释补充。

模块(Module)

JavaScript 有一个很长的处理模块化代码的历史,TypeScript 从 2012 年开始跟进,现在已经实现支持了很多格式。不过随着时间流逝,社区和 JavaScript 规范已经收敛为名为 ES 模块(或者 ES6 模块)的格式,这也就是我们所知道的 import/export 语法。

ES 模块在 2015 年被添加到 JavaScript 规范中,到 2020 年,大部分的 web 浏览器和 JavaScript 运行环境都已经广泛支持。

本章将覆盖讲解 ES 模块和和它之前流行的前身 CommonJS module.exports = 语法,你可以在 Modules 章节找到其他的模块模式。

JavaScript 模块是如何被定义的(How JavaScript Modules are Defined)

在 TypeScript 中,就像在 ECMAScript 2015 中 ,任何包含了一个顶层 import 或者 export 的文件会被认为是一个模块。

相对应的,一个没有顶层导入和导出声明的文件会被认为是一个脚本,它的内容会在全局范围内可用。

模块会在它自己的作用域,而不是在全局作用域里执行。这意味着,在一个模块中声明的变量、函数、类等,对于模块之外的代码都是不可见的,除非你显示的导出这些值。

相对应的,要消费一个从另一个的模块导出的值、函数、类、接口等,它也需要使用导入的格式先被导入。

非模块(Non-modules)

在我们开始之前,我们需要先理解 TypeScript 认为什么是一个模块。JavaScript 规范声明任何没有 export 或者顶层 await 的 JavaScript 文件都应该被认为是一个脚本,而非一个模块。

在一个脚本文件中,变量和类型会被声明在共享的全局作用域,它会被假定你或者使用 outFile 编译选项,将多个输入文件合并成一个输出文件,或者在 HTML使用多个 <script> 标签加载这些文件。

如果你有一个文件,现在没有任何 import 或者 export,但是你希望它被作为模块处理,添加这行代码:

export {};
复制代码

这会把文件改成一个没有导出任何内容的模块,这个语法可以生效,无论你的模块目标是什么。

TypeScript 中的模块(Modules in TypeScript)

在 TypeScript 中,当写一个基于模块的代码时,有三个主要的事情需要考虑:

  • 语法:我想导出或者导入该用什么语法?
  • 模块解析:模块名字(或路径)和硬盘文件之间的关系是什么样的?
  • 模块导出目标:导出的 JavaScript 模块长什么样?

ES 模块语法(ES Module Syntax)

一个文件可以通过 export default 声明一个主要的导出:

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

然后用这种方式导入:

import hello from "./hello.js";
hello();
复制代码

除了默认导出,你可以通过省略 defaultexport 语法导出不止一个变量和函数:

// @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;
}
复制代码

这些可以在其他的文件通过 import 语法引入:

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

附加导入语法(Additional Import Syntax)

一个导入也可以使用类似于 import {old as new} 的格式被重命名:

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

你可以混合使用上面的语法,写成一个单独的 import

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

你可以接受所有的导出对象,然后使用 * as name 把它们放入一个单独的命名空间:

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

// const positivePhi: number
复制代码

你可以通过 import "./file" 导入一个文件,这不会引用任何变量到你当前模块:

// @filename: app.ts
import "./maths.js";
 
console.log("3.14");
复制代码

在这个例子中, import 什么也没干,然而,math.ts 的所有代码都会执行,触发一些影响其他对象的副作用(side-effects)。

TypeScript 具体的 ES 模块语法(TypeScript Specific ES Module Syntax)

类型可以像 JavaScript 值那样,使用相同的语法被导出和导入:

// @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;
复制代码

TypeScript 已经在两个方面拓展了 import 语法,方便类型导入:

导入类型(import type)

// @filename: animal.ts
export type Cat = { breed: string; yearOfBirth: number };
// '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();
复制代码

内置类型导入(Inline type imports)

TypeScript 4.5 也允许单独的导入,你需要使用 type 前缀 ,表明被导入的是一个类型:

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

这些可以运行一个非 TypeScript 编译器比如 Babel、swc 或者 esbuild 知道什么样的导入可以被安全移除。

导入类型和内置类型导入的区别在于一个是导入语法,一个是仅仅导入类型

有 CommonJS 行为的 ES 模块语法(ES Module Syntax with CommonJS Behavior)

TypeScript 的 ES 模块语法跟 CommonJS 和 AMD required 直接相关。使用 ES 模块的导入跟 require 一样都适合大部分的情况。但是这个语法能确保你在有 CommonJS 输出的 TypeScript 文件里,有一个 1 对 1 的匹配:

import fs = require("fs");
const code = fs.readFileSync("hello.ts", "utf8");
复制代码

你可以在模块引用页面了解到关于这个语法更多的信息。

CommonJS 语法(CommonJS Syntax)

CommonJS 是 npm 大部分模块的格式。即使你正在写 ES 模块语法,了解一下 CommonJS 语法的工作原理也会帮助你调试更容易。

导出(Exporting)

通过设置全局 moduleexports 属性,导出标识符。

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

这些文件可以通过一个 require 语句被导入:

const maths = require("maths");
maths.pi;
// any
复制代码

你可以使用 JavaScript 的解构语法简化一点代码:

const { squareTwo } = require("maths");
squareTwo;
// const squareTwo: any
复制代码

CommonJS 和 ES 模块互操作(CommonJS and ES Modules interop)

因为默认导出和模块声明空间对象导出的差异,CommonJS 和 ES 模块不是很合适一起使用。TypeScript 有一个 esModuleInterop 编译选项可以减少两种约束之间的冲突。

TypeScript 模块解析选项(TypeScript’s Module Resolution Options)

模块解析是从 import 或者 require 语句中取出字符串,然后决定字符指向的是哪个文件的过程。

TypeScript 包含两个解析策略:Classic 和 Node。Classic,当编译选项module 不是 commonjs 时的默认选择,包含了向后兼容。Node 策略则复制了 CommonJS 模式下 Nodejs 的运行方式,会对 .ts.d.ts 有额外的检查。

这里有很多 TSConfig 标志可以影响 TypeScript 的模块策略: moduleResolution, baseUrl, paths, rootDirs

关于这些策略工作的完整细节,你可以参考 Module Resolution

TypeScript 模块输出选项(TypeScript’s Module Output Options)

有两个选项可以影响 JavaScript 输出的文件:

  • target 决定了哪个 JS 特性会被降级(被转换成可以在更老的 JavaScript 运行环境使用),哪些则完整保留。
  • module 决定了模块里的那些代码会被用于和彼此互动

你使用哪个 target 取决于你期望代码运行的环境。这些可以是:你需要支持的最老的浏览器,你期望代码运行的最老的 Nodejs 版本,或者一些独特的运行环境约束,比如 Electron 等。

所有模块之间的交流都是通过模块加载器,编译选项 module 决定了哪一种会被用到。在运行时,模块加载器会在执行它之前定位和执行一个模块所有的依赖。

举个例子,这是一个使用 ES Module 语法的 TypeScript 文件,展示了 module 选项不同导致的结果:

import { valueOfPi } from "./constants.js";
 
export const twoPi = valueOfPi * 2;
复制代码

ES2020

import { valueOfPi } from "./constants.js";
export const twoPi = valueOfPi * 2;
复制代码

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;
复制代码

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;
});
 
复制代码

注意 ES2020 已经跟原始的 index.ts 文件相同了。

你可以在 TSConfig 模块页面看到所有可用的选项和它们对应编译后的 JavaScript 代码长什么样。

TypeScript 命名空间(TypeScript namespaces)

TypeScript 有它自己的模块格式,名为 namespaces 。它在 ES 模块标准之前出现。这个语法有一系列的特性,可以用来创建复杂的定义文件,现在依然可以在 DefinitelyTyped 看到。当没有被废弃的时候,命名空间主要的特性都还存在于 ES 模块 ,我们推荐你对齐 JavaScript 方向使用。你可以在命名空间页面了解更多。

TypeScript 系列

  1. TypeScript 之 基础入门
  2. TypeScript 之 常见类型(上)
  3. TypeScript 之 常见类型(下)
  4. TypeScript 之 类型收窄
  5. TypeScript 之 函数
  6. TypeScript 之 对象类型
  7. TypeScript 之 泛型
  8. TypeScript 之 Keyof 操作符
  9. TypeScript 之 Typeof 操作符
  10. TypeScript 之 索引访问类型
  11. TypeScript 之 条件类型
  12. TypeScript 之 映射类型
  13. TypeScript之模板字面量类型
  14. TypeScript之类(上)
  15. TypeScript之类(下)

微信:「mqyqingfeng」,加我进冴羽唯一的读者群。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。

Guess you like

Origin juejin.im/post/7039707412978384932