首先,我们有这样一个简单的项目,它的目录结构是这样的
.
├── package.json
├── src
│ ├── index.ts
│ └── types.ts
├── tsconfig.json
└── yarn.lock
我们的types.ts里的内容非常简单
export type UserConfig = {
apiToken: string;
uuid: string;
};
接下来,我们分两种情况,在index.ts中引入UserConfig。
Ⅰ 引入,并在index.ts作为类型使用
import { UserConfig } from './types';
export const userConfig: UserConfig = {
apiToken: '114514',
uuid: '1919810'
}
Ⅱ 引入,在index.ts中直接直接导出, Ⅲ直接重导出
import { UserConfig } from './types';
export { UserConfig }
export { UserConfig } from './types';
显然,我们知道, Typescript在生成Javascript的代码时,会擦除我们定义的Typescript类型,在运行时,Typescript的类型系统是不存在的。
对于仅做类型擦除的编译器,比如被广泛使用的Babel,它对.ts文件的类型分析,仅限于当前文件,并没有跨文件分析的能力。那么对于上面三种index.ts,它就会做这样的处理。
对于Ⅰ,transfomer到了UserConfig是一个Typescript的类型使用的,因此移除它的所有定义和引用。显然,它是被import { UserConfig } from './types'定义的,那么,删除掉关于UserConfig的引入。
最后Babel编译出来的代码是这样的
const userConfig = {
apiToken: '114514',
uuid: '1919810'
}
对于单个模块(文件)如果它的导入导出是确定的, 我们称呼他们是模块隔离的
但对于Ⅱ和Ⅲ,Babel并没有办法从单个index.ts文件中分析出,UserConfig是一个类型。那么经过Babel处理之后它就会原汁原味保留下来。
然而下面的代码会在Webpack中提示模块types 并没有导出成员 UserConfig,在Vite中会直接引起浏览器的Runtime error
import { UserConfig } from './types';
export { UserConfig }
export { UserConfig } from './types';
显然,这两种index.ts并不是模块隔离的。因为UserConfig不确定是Typescript中类型还是JavaScript运行时的值
除了引入类型不使用,重导出类型之外,还有两种情形也会导致模块隔离被破坏
- 引入并使用了const enum的值
- Namespaces,同名的namespace可以跨越多个文件,最后编译时他们会被
tsc合并,这种跨文件的文件处理babel是做不到的
那么,isolateModules选项的功能也就很自然能理解了。
它会强制开发者的每个模块都能作为单个模块独立编译。当这种保证存在时,我们可以选择babel, swc, esbuild等仅做类型擦除的单文件的编译器。
那么,当我们打开isolateModule时,我们要移除const enum(用普通的enum代替),和跨文件namespace的使用,对于上面的Ⅱ和Ⅲ,我们也需要修改一下。
这种修改的核心思路就是,既然编译器不知道,那么就显式告诉编译器,我们引入或导出的内容,就是一个类型
// 法1
import type { UserConfig } from './types';
export { UserConfig };
// 法2
import { UserConfig } from './types';
export type { UserConfig }
export type { UserConfig } from './types'
结论
下面这些情况会破坏模块的隔离性
- 从其它模块类型后未使用该类型
- 重导出(
expot { Type } from)其它模块的类型 - 引入其它模块的const enum并使用
- 使用namespace语法
isolateMododules
- 开启后会强制要求开发者保持模块的隔离性
- 如果使用
babel,swc等非tsc编译器,强烈推荐打开isolateModules来避免潜在的Runtime error - 如果一个类型导入后不被使用,请使用
import type { SomeType } from 'module',告诉编译器你导入的是一个类型 - 如果需要导出类型,请使用
export type { SomeType },但如果引入时使用import type,那么也可以直接export { SomeType } - 如果需要重导出类型,请使用
export type { SomeType } from 'module' - 所有
import type和import type类型会告诉编译器,导入的是一个类型,他们都要最终编译产物中移除
关于tsc
tsc的类型分析是项目级的,因此比起其它单文件分析类型的编译器,它能比其它的知道更多类型信息,因此对于非模块隔离的代码也能灵活处理- 如果使用ts-loader, 它会用tsc分析整个项目。然而当它的
transpileOnly选项为true时,它也将降级为仅对当前文件进行分析,此时也必须要求模块的隔离性,请启用isolateModules - 项目级的类型分析比单文件的类型分析更加强大,但代价就是它的运行速度会比单文件分析更慢。