科技微讯

Typescript 引入模块的三种写法

Typescript 是 javascript 的超集,js 有的功能 ts 也有。这意味着 ts 在引入一个模块时,如果 js 同时支持 CommonJS 和 ESM,那 ts 也同时支持;如果 js 仅支持 ESM,那 ts 也仅支持 ESM。

js 支不支持 CommonJS 或 ESM 主要和它的执行环境有关。Node.js 从 12 开始支持 ESM,15.3 才稳定支持,而 CommonJS 它一直都支持。浏览器对 ESM 的支持时间各不相同,Chrome 是从 2017 年开始支持 ESM,浏览器一直不支持 CommonJS。

js 不支持 ESM 或者 CommonJS 并不代表你不能写 CommonJS 或 ESM,你可以写 CommonJS,也可以写 ESM,最后一律用 babel 或 swc 把它转换为目标执行环境支持的方案。

作为超集,ts 还支持一些独有的引入语法。

const...require...

const path = require("path");

如果提示 require 错误,可能是因为没有安装@types/node

@typescript-eslint 默认开启 no-var-requires 规则,即默认不推荐使用这种方法。

如果 .tsconfig.json 中的 compilerOptions.module 设置为 ESNext 则不能用 require,这时 tsc 会把 ts 转换为 ESM 下的 js,而 ESM 不支持 require。开头就说过,js 支持的 ts 也支持,js 不支持的 ts 也不支持。除非可以通过 babel、swc 等工具把 require 转为 import

import...from...

这是 ESM 的标准语法,也是 Typescript 最常用的写法。使用 import 导入 Node.js 内置模块或一些第三方 CommonJS 模块可能需要额外设置。

比如 path

import path from "path";

这样可能会报错:

This module is declared with 'export =', and can only be used with a default import when using the 'esModuleInterop' flag.

在 tsconfig.json 中把 compilerOptions.esModuleInterop 设置为 true 可解决该问题。

再看 fs

import fs from "fs";

可能会报错:

Module '"fs"' has no default export.

同样把 esModuleInterop 设置为 true 可解决该问题,否则需要修改 import 的方式:

import * as fs from "fs";

import...require...

上面 path 的报错提示 path 是通过 export = 导出的,如果不想把 esModuleInterop 设为 true必须这样写

import path = require("path");

When exporting a module using export =, TypeScript-specific import module = require("module") must be used to import the module. 出处

export =import = require() 都是 ts 的独有语法,起码在 Typescript 1.0 就支持。

@typescript-eslint 默认开启 no-require-imports 规则,即默认不推荐使用这种方法。

import type

值得一提的是,ts 在 import 一个 type 时,建议使用 import type,很明显这也是 Typescript 的独有语法。

// Re-using the same import
import { APIResponseType } from "./api";
// Explicitly use import type
import type { APIResponseType } from "./api";
// Explicitly pull out a value (getResponse) and a type (APIResponseType)
import { getResponse, type APIResponseType } from "./api";

用 import 还是 require

Node.js 同时支持 CommonJS、ES Module,浏览器只支持 ES Module,那是不是意味着 Gatsby.js、Next.js 这些框架不能写 require 呢?非也。无论项目是不是用 ts 写,Gatsby.js、Next.js 可以在页面或组件中同时使用 requireimport,甚至可以在同一个组件混着用这两种方法。因为开发者写的代码并不是直接运行在 Node.js 或浏览器中,而是都会被 compiler 转换后再执行。

同理,ts 写的代码也不是直接运行在 Node.js 或浏览器中,.ts.tsx 会被 compiler 先转换为标准的 js 代码,这时 import 可以被转换为 require,或者反过来。

tsconfig.json 有一个 compilerOptions.module 字段,设置为 commonjs 时,Typescript 官方 compiler tsc 会把 import 转换为 require,设置为 ESNextES2020 等值时,import 会被保留。tsc 似乎不会把 require 转换为 import,这需要使用 babel、swc 等其他 compiler。

在 Next.js 中,compilerOptions.jsx 的值被设置为 preserve,这样 tsc 只是把 .tsx 转换为 .jsx,后续 .jsx 转换为 .js 就交给 swc 或 babel。swc 既可以把代码转换为 CommonJS,也可以转换为 ESM。babel 也是。在 swc、babel 的支持下,import 不仅可以用于 js 文件的引入,还可用来引入 image 等文件。

位于 Gatsby.js 或 Next.js 根目录的配置文件例如 next.config.js 由于不会被 babel 或 swc compile,默认只能用 require,这也是 Next.js 不支持 next.config.ts 的原因。从 Next.js 12 开始 next.config.js 可以改为 next.config.mjs,这样就能用 import 了,因为 .mjs 后缀把这个文件从一个 CommonJS 模块变成了 ESM 模块。

虽然在 ts 中既可以用 require 也可以用 import 甚至还能混着用,但 @typescript-eslint 不推荐使用 require(),主要原因可能是 import 毕竟是 ECMAScript 标准内的东西,而且 ts 从 2012 年刚发布时就开始使用 import 了,ESM 可能也是受到 Typescript 的启发吧?另外 ts 的独有写法 import typeimport=require() 也用 import,为了让代码统一风格,一律用 import 应该是比较好的做法。关于用 import 还是 require,可以再看看 stackoverflow 的一个帖子

ts-node

ts-node 可以用来直接执行 .ts 文件,它的工作原理是在后台把 .ts 转换为 .js,再调用 Node.js 去执行。在 npm script 中使用 ts-node,ts-node 默认会使用 tsconfig.jsonpackage.json 的配置,如果 compilerOptions.module 被设置为 esnext,则 ts-node 会把 .ts 转换为 ESM 的 .js,接下来用 node 去执行这个 .js 文件,有三种情况:

compilerOptions.module 如果设置为 commonjs,那 ts-node 可以直接执行 .ts 文件。ts-node 也可以通过 --skipProject 参数忽略 tsconfig.json 配置,直接执行 .ts 文件,它会默认把 .ts 转换为 commonjs 的 .js,Node.js 默认支持 commonjs,所以不需要任何其他设置。

Next.js 的配置文件不支持使用 Typescript,但 jest 的配置文件 jest.config.ts 可以用 Typescript 写,但需要额外安装 ts-node。

总结

Typescript 支持哪种模块引入方式,和 tsc 把 ts 代码转换为哪个版本的 js 代码有关(体现在 .tsconfig.json 的配置),也和最后 js 代码的运行环境有关(浏览器还是 Node.js,版本是啥),并且和是否使用了 babel、swc 等 compiler 有关。


相关笔记:

相关阅读:

暂无评论