TS 导入导出那些事
前言
最近用 TypeScript 写 npm 包,各种模块、命名空间、全局定义等等扰得我睡不着觉。
我便苦心研究,总结了几个比较冷门的,国内貌似基本上找不到资料的导入导出用法,顺便在其中又插入一些不那么冷门的用法,于是本篇文章来了。
因为一开始也没想做成大全,可能之后还会继续更新吧。
目录
导入模块
导入模块主要分为 3 种:
导入ESM模块
导入平常的 ES Module 中的东西相信大家都不陌生。唯一需要注意的便是默认导出与“星号”式导入的区别。
“星号”式导入:
import * as Mod from './mod';
// 类似于 JS 中的
const Mod = require('./mod');
导入模块的默认导出:
import ModDef from './mod';
// 类似于 JS 中的
const { default: ModDef } = require('./mod');
命名的方式导入模块:
import ModDef, { a, b } from './mod';
// 类似于 JS 中的
const {
default: ModDef,
a, b
} = require('./mod');
导入CJS模块
导入 CommonJS 模块和 ES Module 稍有不同。主要是因为面向 CJS 的模块会有导出分配。
如果最终模块是个对象,则可以使用同 ESM 一样的方式导入。
import * as Mod from './mod';
如果模块是个函数或者基本数据类型之类的,则要用以下这种方式
import Mod = require('./mod');
实际上具体使用哪种还是有点混乱的,主旨思想是只要引入不报错就行。
类型导入
除了上面那两种,还有一种类型导入。
这种导入的独特之处是它只导入类型,所以不会真的去导入模块。这一点可以看编译后的文件。
import type { abc } from './hh';
import { type cba } from './hhh';
import { type abcba, efgh } from './hhhh';
efgh;
// 编译后
const hhhh_1 = require('./hhhh');
hhhh_1.efgh;
除此之外, JS 中也可以通过 JSDoc 的形式来进行类型引入。
这个特性的添加可以说是跌宕起伏,总之现在是能用了:
/**@import { abc } from './hh'; */
/**@type {abc} */
const typed = 'abc';
在模块中导出
在模块中导出东西相信大家也不陌生,不过这里还是详细讲解一下。
在模块中导出东西有很多种方法。导出总共可分为 4 类:
命名导出
命名导出有两种方法,一种是声明着导出
export namespace A { }
export function b() { }
export class C { }
export const d = 123;
export let { e } = { e: 'hh' };
一种是声明后导出
namespace A { }
function b() { }
class C { }
const d = 123;
let { e } = { e: 'hh' };
export { A, b, C, d, e };
声明后导出比声明着导出更灵活,能合并,也能重命名
namespace A { }
export { A };
function b() { }
export { b as c };
class C { }
export { C as B };
const d = 123;
let { e } = { e: 'hh' };
export { d, e };
命名导出编译成 Common JS 后类似这样
exports.xxx = xxx;
需要注意的是其他人无法修改任何你导出的东西。即使是使用 let
声明也一样
/* mod.ts */
export let a = 123;
/* others.ts */
import { a } from './mod';
a = 321; // 报错:ts(2632)
不过对于上面的代码,你可以随便修改所导出的 a
。因为其他人每次读取 a
时都会重新从你的导出对象上访问一次 a
属性,不用担心其他人无法接收到你的修改。具体可以查看编译后的 JS 文件
import { a } from './mod';
const b = a + 123;
console.log(a);
// 编译后
var mod_1 = require("./mod");
var b = mod_1.a + 123;
console.log(mod_1.a);
默认导出
默认导出可以理解为一种特殊的命名导出。
默认导出的名字是 default
。但是你不能搞个名字叫 default
的变量然后导出,你必须得用 export default
或者在导出时重命名
export let default = 123; // 报错:ts(1389)
export default 123; // 正确
export let a = 123;
export { a as default }; // 正确
星号导入搭配默认导出,可以达到默认导出即为星号导出的效果
/* mod.ts */
import * as Self from './mod';
export default Self;
// 或者
export * as default from './mod';
/* others.ts */
import * as Mod from './mod';
import ModDef from './mod';
console.log(Mod === ModDef); // true
导出分配
导出分配就是把 Common JS 的导出搬到了 TS 中。写法也差不多
export = 'hh';
// 相当于
module.export = 'hh';
导出分配也可以指定默认导出,只需要有 default
属性就可以
/* mod.ts */
export = { default: 123 };
/* others.ts */
import mod from './mod';
console.log(mod); // 123
需要注意的是采用了导出分配后便不能再使用其他导出方法。
导出其他模块
导出其他模块一般分为两种。
第一种是星号导出,把其他模块的东西全导出到自己里头,就像
export * from './xxx'
具体效果是 xxx
中导出的东西,也可以通过你访问到。
/* xxx.ts */
export let a = { hh: 'hh' };
/* mod.ts */
export * from './xxx.ts';
/* others.ts */
import { a } from './xxx';
import { a } from './mod';
console.log(a === a); // true
第二种是挂到自己模块下面,既可以是这样
export * as xxx from './xxx';
// 编译后
exports.xxx = require('./xxx');
也可以是这样
export import xxx = require('./xxx');
// 编译后
var xxx = require('./xxx');
exports.xxx = xxx;
二者主要的区别在于前者仅仅是导出了模块,其他什么都没干;而后者实际上还引入了模块,为模块注册了标识符,让你在导出的同时还可以通过 xxx
访问到 ./xxx 模块。
如果只想把另一个模块的一部分东西导出出去,还有这种方法
export {
alpha,
default as beta,
} from './xxx';
导入命名空间
虽然现在命名空间相较于模块并不是特别常用,但它还是有比较完整的导入导出功能的。
导入命名空间中的东西很简单,像这样
import xxx = Space.xxx;
不论你是在模块中导入全局命名空间,还是在命名空间中导入其他命名空间,都是适用的。
import Err = globalThis.Error;
throw Err('hh');
namespace A {
import Process = NodeJS.Process;
let proce: Process;
}
较为可惜的是命名空间貌似没有星号导入,也不支持解构导入。
在命名空间中导出
在一般 TS 中,命名空间只有两种方法导出。
第一种方法是声明着导出,类似于模块
namespace A {
export const a = 123;
}
第二种方法是导入着导出,可以用来导出其他命名空间的东西
namespace A {
export import Err = globalThis.Error;
}
而对于不一般的 TS ——也就是类型声明中,命名空间还可以采用像模块一样的导出对象
declare namespace A {
const a = 123;
const b = 'hh';
export { a, b };
}
使用全局定义
全局定义一般有三种:
-
内置在 TS 中的全局定义。比如
setTimeout
、Error
等。
对于这种全局定义,直接拿来用就可以了。 -
位于环境模块中的全局定义。比如
NodeJS.Process
等。包括位于
node_modules/@types
文件夹中的自动引入的环境模块,都可以通过三斜杠注释来引入。你可以通过
path
直接指定文件路径/// <reference path="./types.d.ts" />
-
位于模块中的全局定义。
这种全局定义只需要引入一下模块,表示你已经运行此模块,即可
import '@babel/core';
或者你也可以通过三斜杠注释,通过
types
指定模块/// <reference types="@babel/core" />
需要注意的是,不论你采用
import
还是三斜杠注释,甚至只是在类型中使用了一个typeof import('xxx')
,只要你在一个 TS 文件中引入了这个模块所定义的全局类型,那这个类型就会永远存在下去,污染你的globalThis
。
唯一在不污染全局域的情况下运行模块的方法是使用import()
函数动态引入,但这样子你也拿不到你需要的类型。
进行全局定义
进行全局定义一般有三种方法。
第一种是直接写环境模块。不带任何 import
和 export
一般就会让编译器把这当成一个环境模块。所以,如果你需要防止一个 TS 文件变成环境模块导致类型泄露的话,你可以加一个安全无公害的 export { };
。
第二种是在模块中定义,只需要把类型定义写到 declare global
里头就行
declare global {
const a = 123;
let b: {};
}
a; // 123
b; // {}
第三种是通过合并 globalThis
命名空间来定义,好处是可以使用命名空间的“导入着导出”方法,将模块或者其他命名空间的局部变量变成全局变量。这样子就可以避免命名空间在定义到全局时变成一个值导致的类型定义的丢失。
import Mod from './mod';
declare global {
namespace globalThis {
const a = Mod;
export import b = Mod;
}
}
a; // typeof import("./mod")
b; // module "./mod"
博客园原文链接:https://www.cnblogs.com/QiFande/p/ts-about-export.html,转载请注明。
如果你对本篇文章感兴趣,不如来看看肉丁土豆表的其他文章,说不定也有你喜欢的。