模块中的类型
本教程共 80 篇 · 第 55 篇 · 更新于 2026-08-10 · 约 8 分钟阅读
本节目标:掌握 TypeScript 在模块中处理类型的独特语法——
import type、export type和内联 type 导入。理解它们和普通导入的区别,以及为什么编译后类型导入会消失。
前一章讲了 ES 模块的导入导出语法。TypeScript 在标准 JS 模块的基础上加了一套专门处理类型的导入导出机制。这套机制的核心目的很简单:告诉编译器和打包工具,哪些导入纯属类型,编译后可以安全删除。
类型可以和值一起导出
最基础的用法——把类型和值写在同一个 export 里:
// animal.ts
export interface Cat {
breed: string;
yearOfBirth: number;
}
export type Dog = {
breeds: string[];
yearOfBirth: number;
};
export function createCatName(): string {
return "fluffy";
}
导入方不需要区分哪个是类型、哪个是值:
// app.ts
import { Cat, Dog, createCatName } from "./animal.js";
type Animals = Cat | Dog;
const name = createCatName();
TypeScript 编译器自己知道 Cat 和 Dog 是类型、createCatName 是值。编译成 JavaScript 后,Cat 和 Dog 会被删除,只留下 createCatName 的导入。
但打包工具不知道。esbuild、swc、Babel 这些工具只做语法转换,不做类型检查。它们看到 import { Cat, Dog, createCatName },无法判断谁该删谁该留——除非你明确标记。
import type:只导入类型
TypeScript 3.8 引入了 import type。它明确告诉所有人——编译器、打包工具、读代码的同事——这个导入只引入类型,运行时不产生任何东西:
// animal.ts
export type Cat = { breed: string; yearOfBirth: number };
export type Dog = { breeds: string[]; yearOfBirth: number };
export const createCatName = () => "fluffy";
// app.ts
import type { Cat, Dog } from "./animal.js";
export type Animals = Cat | Dog;
编译成 JavaScript 后,整个 import type 行直接消失——它根本不会出现在输出文件里。
import type 的关键约束:你不能把类型导入当成值来用:
import type { createCatName } from "./animal.js";
const name = createCatName();
// ❌ 报错:'createCatName' 是作为类型导入的,不能当值使用
这个错误是 TypeScript 特意设计的保护机制——如果你把值标记成 import type,编译器会立刻提醒你。它避免了”我以为导入了值,结果编译后没了”的暗坑。
内联 type 导入(TS 4.5+)
TypeScript 4.5 增加了一种更细粒度的写法——在同一个 import 语句里,只给特定的导入项加上 type 前缀:
import { createCatName, type Cat, type Dog } from "./animal.js";
export type Animals = Cat | Dog;
const name = createCatName();
这样一行导入同时包含值和类型,打包工具看到 type 前缀就知道哪几项可以安全删除。createCatName 没有 type 前缀,所以它会被保留。
哪种更好?团队偏好决定。内联式更紧凑,import type 更清晰——一眼就知道这个导入全是类型。我个人建议:如果整个导入里类型超过一半,用 import type;如果只有一两个类型混在值里,用内联式。
export type:只导出类型
同样,导出端也有类型专用语法:
// types.ts
export type { Cat, Dog };
和 import type 一样,export type 也在编译后消失。重导出场景里特别有用:
// index.ts——库的入口文件
export type { Cat, Dog } from "./animal.js";
export { createCatName } from "./animal.js";
这样你的类型和值分别导出,打包工具可以精确地只保留值的导入。
import type vs 普通 import
很多同学会问:反正 TypeScript 编译器能自己分辨类型和值,为什么还要用 import type?这里列出几个实际差异:
1. 编译产物不同。 普通 import 即使只导入了类型,在某些配置下也会在 JS 输出里留下痕迹。import type 保证编译后完全消失。
2. 打包工具友好。 esbuild、swc、webpack 的 TypeScript loader 在做 tree-shaking 时,import type 给了它们明确的”可以删”信号。普通导入它们不敢乱删——万一那个导入有副作用呢?
3. 避免循环引用问题。 在类型层面,模块之间的循环引用很常见(A 引用 B 的类型,B 引用 A 的类型)。import type 只发生在类型空间,不参与运行时模块加载,所以不会造成 ReferenceError。
4. isolatedModules 兼容。 如果你开启了 isolatedModules(TS 7.0 里很多项目都开着),有些写法必须用 import type:
// ❌ isolatedModules 下会报错
export { Cat } from "./animal.js";
// ✅ 必须明确标注
export type { Cat } from "./animal.js";
因为 isolatedModules 要求每个文件独立可编译,TypeScript 无法跨文件推断 Cat 是类型还是值,所以你必须自己声明。
小结
import type 和 export type 是 TypeScript 模块体系里”轻量但实用”的改进。它们解决的核心问题是让非 TS 工具也能理解”这是个类型、可以删”,从而产出更干净的编译产物。日常开发里,养成给类型导入加 type 标记的习惯,既帮了打包工具,也帮了几个月后回来看代码的自己。