模块(ESM)
书中这部分内容和 JavaScript 知识有很多重复,因此基础的不做讲解,同时需要注意的点可以看 JavaScript module 来学习,这里值讨论 TypeScript 特有的模块导入和导出语法
TypeScript 中类和枚举可以同时表示一个类和一个类型,因此在使用 import 导入一个类或者枚举的时候,会同时导入他们表示的类型和值。在 TypeScript 3.8 中,引入了只针对类型的导入语法
import type { SomeType } from "./some-module";
// 这里 SomeType 只会被导入为类型,不会导入值并且在编译时,所有的类型导入都会被删除,他们只是方便我们进行类型检查,显式的导入我们需要的类型
NOTE
说到类型导入会被删除,我们可以想一下如果导入一个带有副作用的模块会发生什么
import { Point } from "./point";
const p: Point = { x: 1, y: 2 };
if (globalThis.ctrl) {
console.log(p);
}globalThis.ctrl = true;
export interface Point {
x: number;
y: number;
}虽然 TypeScript 源代码中导入了 point.ts 文件,但是实际上编译后的代码中并没有导入 point.js 文件,因为 Point 只是作为一个类型被使用,就被莫名其妙的 treeshaking 了! 如果非要这个副作用不可,可以使用 import "./point" 再导入一次,这是最简洁有效的方法,当然也可以设置 tsconfig.json 中的 verbatimModuleSyntax 字段,这是 TypeScript 5.0 引入的新选项。其规则更为简单,任何未使用类型修饰符的导入或导出都会保留,而使用了类型修饰符的内容则会被完全移除。 你可能看到有些教程让使用 preserveValueImports 或者 importsNotUsedAsValues,很遗憾,TypeScript 承认了这两个选项界限比较模糊,难以理解,因此在这个版本中将他们废弃了!
--module 选项
TypeScript 编译器提供了 --module 选项来指定输出的模块规范,可以是 CommonJS、AMD、System、UMD、ES6、ES2015、ESNext 等,这个选项会影响到模块导入和导出的语法,比如 CommonJS 中使用 module.exports 导出,require 导入,而 ES6 中使用 export 导出,import 导入
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.twoPi = void 0;
const constants_1 = require("./constants");
exports.twoPi = constants_1.valueOfPi * 2;给一个编译成 CommonJS 以后的例子,注意到其中有一个 __esModule,这是表明该文件是由 ESM 编译而来的
外部声明
TypeScript 外部声明分为两种
外部类型声明 他又分为下面五种
外部变量声明 语法如下:
typescriptdeclare var a: boolean; declare let b: number; declare const c: string;只举出这一个例子,其他的可以自己搜一下,和普通的声明差不多,只不过多了
declare关键字,还有不能有定义,只能有声明外部函数声明
外部类声明
外部枚举声明
外部命名空间声明
外部模块声明 语法如下:
typescriptdeclare module "url" { export function parse( urlStr: string, parseQueryString?, slashesDenoteHost?, ): Url; }
类型声明文件
TypeScript 中的 .d.ts 类型声明文件主要来源为:
- 内置的声明文件,比如
lib.d.ts - 通过
@types包来安装的第三方库的声明文件 - 自定义的声明文件
如果作为一个库的开发者,我们肯定希望能够给用户提供完善的类型声明文件,这不仅需要 TypeScript 编译出类型声明文件,还需要在 package.json 中指定声明文件的入口路径,这是通过 types 或者 typings 字段来指定的,比如:
{
"name": "awesome-lib",
"types": "dist/index.d.ts"
}这两个字段是 TypeScript 拓展的,并不是标准的 package.json 字段,但是 TypeScript 会读取这两个字段来确定类型声明文件的位置。
还有一个特例,如果一个包的类型声明文件是 index.d.ts,那么可以省略 types 或者 typings 字段,TypeScript 会自动去找 index.d.ts 文件。
模块解析
当我们导入一个模块时,TypeScript 编译器会去根据一定的规则去寻找这个模块的定义,这就是所谓的模块解析策略,并且这和编辑器的 LSP 有很大关系,定义良好的模块解析策略可以让 LSP 充分发挥他的作用!
首先说模块导入的分类,他分为相对导入和绝对导入,这里的相对请不要理解成相对路径的那种相对,他表示导入时是否以 /、./、../ 开头,如果是,那么就是相对导入,否则就是绝对导入
import { a } from "./module"; // 相对导入
import { b } from "../module"; // 相对导入
import { c } from "module"; // 绝对导入
import { d } from "@angular/core"; // 绝对导入TypeScript 提供了多种模块解析策略,我们可以通过 moduleResolution 选项来指定,他有以下几种值:
Node16:或者NodeNext,支持 Node.js 的 ECMAScript 模块解析,遵循package.json中的"type": "module"配置Node:先查找.ts、.tsx、.d.ts文件,然后查找package.json中的 types 字段,最后查找index.ts、index.tsx、index.d.tsClassic:不推荐使用Bundler:类似node16,但支持package.json的exports和imports字段
由于我们通常使用比较新的开发工具,因此请尽可能 Node16 或者 NodeNext 或者 Bundler,推荐配置:
现代 Web 应用程序:module: "ESNext", moduleResolution: "Bundler" Node.js 应用程序:module: "NodeNext", moduleResolution: "NodeNext"
TIP
与 module 选项的关系:
当 module 设置为 CommonJS 时,默认 moduleResolution 为 Node 当 module 设置为 ES2015/ES2020/ESNext 时,建议将 moduleResolution 设置为 Node16/NodeNext 或 Bundler
扩充模块声明
有时候我们需要给一个已经存在的模块添加一些属性,这时候就需要使用扩充模块声明,比如我们想给 express 添加一个 static 方法,可以这样写:
declare module "express" {
export interface Application {
static: (path: string, root: string) => void;
}
}这就会利用类型合并的知识点来给 express 添加一个 static 方法
有时候想要全局的一些类型,可以使用 global 关键字,比如:
declare global {
interface Window {
myGlobal: string;
}
var variable: string; // 这样我们就可以 globalThis.variable 来访问这个变量
}这里不可以用 let 替换 var,因为 let 声明的变量并不是放在 globalThis 下的,详情可以看 执行上下文