首先,我们有这样一个简单的项目,它的目录结构是这样的
.
├── 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
- 项目级的类型分析比单文件的类型分析更加强大,但代价就是它的运行速度会比单文件分析更慢。