TypeScript 学习笔记:全面理解 export、import、declare 和模块导出
在 TypeScript 中,export 用来将当前模块中的函数、变量、类、接口、类型等暴露给其他模块使用。
对于 Go 后端开发者,可以先这样理解:
- Go:通过标识符首字母大写,决定能否被其他包访问。
- TypeScript:通过
export显式指定哪些内容可以被其他模块导入。 - import:从其他模块中导入需要使用的内容。
一、最基本的 export 用法
假设项目结构如下:
project/
├── math.ts
└── main.ts
1. 导出函数
math.ts:
export function add(a: number, b: number): number {
return a + b;
}
export function sub(a: number, b: number): number {
return a - b;
}
// 没有 export,只能在当前模块内部使用
function multiply(a: number, b: number): number {
return a * b;
}
main.ts:
import { add, sub } from "./math";
console.log(add(1, 2)); // 3
console.log(sub(5, 3)); // 2
这里的 import { add, sub } 表示按照导出名称获取相应函数。
需要注意,multiply 没有导出,因此无法通过命名导入直接访问。
2. 导出变量
// config.ts
export const PORT: number = 8080;
export let environment: string = "development";
使用:
import { PORT, environment } from "./config";
console.log(PORT); // 8080
console.log(environment); // development
3. 导出类
// user.ts
export class User {
constructor(public name: string) {}
sayHello(): void {
console.log(`Hello, ${this.name}`);
}
}
使用:
import { User } from "./user";
const user = new User("张三");
user.sayHello();
这与 Go 中导出一个结构体类型有些相似,不过 TypeScript 的 class 还包括构造函数和方法等运行时行为。
二、export default:默认导出
export default 表示将某个值设置为当前模块的默认导出。
一个模块可以有多个命名导出,但只能有一个默认导出。
例如:
// math.ts
export default function add(a: number, b: number): number {
return a + b;
}
导入时:
import calculate from "./math";
console.log(calculate(1, 2)); // 3
为什么明明定义的是 add,却可以使用 calculate?
因为:
import calculate from "./math";
不是根据 calculate 这个名称搜索函数,而是从 math.ts 中获取它的默认导出,并在当前文件将其命名为 calculate。
也可以拆开写:
// math.ts
function add(a: number, b: number): number {
return a + b;
}
export default add;
效果相同。
命名导出和默认导出的区别
| 特性 | export | export default |
|---|---|---|
| 导出数量 | 可以多个 | 每个模块只能一个 |
| 导入语法 | import { add } | import add |
| 导入时改名 | import { add as sum } | import sum |
| 是否依赖导出名称 | 是 | 不依赖原名称 |
| 常见用途 | 工具函数、类型、常量 | 模块的主要对象 |
混合导出
// math.ts
export const PI = 3.14;
export function add(a: number, b: number): number {
return a + b;
}
export default function multiply(a: number, b: number): number {
return a * b;
}
导入:
import multiply, { PI, add } from "./math";
console.log(PI); // 3.14
console.log(add(1, 2)); // 3
console.log(multiply(2, 3)); // 6
三、export declare:声明外部已经存在的内容
这一部分比较重要,也是阅读 TypeScript 工程源码时经常遇到的写法。
先介绍 declare。
1. declare 的含义
declare 表示:
告诉 TypeScript 编译器,某个变量、函数或其他声明已经存在,只需要知道它的类型,不需要在这里创建运行时代码。
例如:
declare const APP_VERSION: string;
它的意思是:
假设运行环境中已经存在一个叫 APP_VERSION 的字符串变量,请在类型检查时认可它。
但 declare 不会真正创建这个变量。
比如:
declare const APP_VERSION: string;
console.log(APP_VERSION);
TypeScript 类型检查可以通过,但如果运行环境没有定义 APP_VERSION,执行时就会报错:
ReferenceError: APP_VERSION is not defined
因此,declare 只负责类型声明,不负责创建实际的变量或函数。
2. export declare function
例如:
// math.d.ts
export declare function add(
a: number,
b: number
): number;
拆开理解:
export:允许其他模块导入这个声明。declare:仅声明函数的类型,不提供函数实现。function add:声明一个名为add的函数。: number:函数返回数字。
其他文件可以这样写:
import { add } from "./math";
const result = add(1, 2);
但要特别注意:math.d.ts 只提供类型声明,并不包含可执行的 add 实现。
真实运行时必须有对应模块提供 add 函数,否则无法正常运行。
3. 为什么需要 export declare?
假设你有一个 JavaScript 文件:
math.js:
function add(a, b) {
return a + b;
}
export { add };
TypeScript 项目想使用这个 JavaScript 模块,同时希望获得类型检查,就可以添加声明文件:
math.d.ts:
export declare function add(
a: number,
b: number
): number;
TypeScript 就知道:
import { add } from "./math";
add(1, 2); // 正确
add("1", "2"); // 类型错误
实际的函数由 math.js 提供,类型由 math.d.ts 描述。
4. .ts 和 .d.ts 有什么区别?
| 文件类型 | 作用 | 通常包含实现吗? |
|---|---|---|
.ts | TypeScript 源代码 | 是 |
.d.ts | TypeScript 类型声明文件 | 否 |
.js | JavaScript 运行时代码 | 是 |
你可以类比 Go 开发时的两个概念:
- 函数签名:描述输入参数和返回类型。
- 函数实现:提供真正的执行逻辑。
TypeScript 的声明文件主要负责前者。
不过 Go 并没有完全对应 .d.ts 的常规模块机制,所以这只是帮助理解的类比。
5. export declare class
声明类也是一样:
// user.d.ts
export declare class User {
name: string;
constructor(name: string);
sayHello(): void;
}
这里只描述:
- 存在一个
User类。 - 有
name属性。 - 构造函数接收一个字符串。
- 有
sayHello()方法。
不会生成 User 类的实际实现。
6. export declare const
// config.d.ts
export declare const PORT: number;
export declare const API_URL: string;
表示对应模块在运行时会提供这些变量,但声明文件本身不会创建它们。
四、export interface:导出接口
TypeScript 的 interface 用来描述对象的结构。
例如:
// user.ts
export interface User {
id: number;
name: string;
age: number;
}
其他文件:
import type { User } from "./user";
const user: User = {
id: 1001,
name: "张三",
age: 18
};
你可以将这种接口理解为对数据结构的一种约束。
类似 Go 的:
type User struct {
ID int64
Name string
Age int
}
但两者不完全相同:TypeScript 的 interface 只参与类型检查,不会生成运行时的结构定义;Go 的 struct 则是具体的数据类型。
注意: interface 本身就属于类型声明,因此通常不需要写 export declare interface 。
五、export type:导出类型
type 用来定义类型别名,也可以描述联合类型、对象类型等。
例如:
// types.ts
export type UserId = number;
export type Status = "pending" | "running" | "success";
export type User = {
id: UserId;
name: string;
};
其他文件:
import type { UserId, Status, User } from "./types";
const id: UserId = 1001;
const status: Status = "running";
const user: User = {
id: 1001,
name: "张三"
};
Status 这里表示一个联合类型,只允许指定的三个字符串。
例如:
const status: Status = "failed";
会发生类型错误,因为 "failed" 不属于定义的联合类型。
export type 与 import type
除了:
export type UserId = number;
还可以写:
// types.ts
type UserId = number;
type UserName = string;
export type { UserId, UserName };
使用:
import type { UserId, UserName } from "./types";
import type 表示只导入类型,不会生成对应的运行时导入。
这在大型 TypeScript 项目中非常常见。
六、export { }:集中导出
前面都是在声明函数时直接加 export:
export function add(a: number, b: number) {
return a + b;
}
其实还可以在文件最后集中导出。
// math.ts
function add(a: number, b: number) {
return a + b;
}
function sub(a: number, b: number) {
return a - b;
}
const PI = 3.14;
export { add, sub, PI };
导入:
import { add, sub, PI } from "./math";
这两种方式的效果基本一致。
导出时重新命名
function add(a: number, b: number) {
return a + b;
}
export { add as sum };
其他文件:
import { sum } from "./math";
console.log(sum(1, 2)); // 3
注意这里对外暴露的是 sum,而不是 add。
七、export * from:批量重新导出
在大型项目中,经常有很多文件。
例如:
project/
├── utils/
│ ├── math.ts
│ ├── string.ts
│ └── index.ts
└── main.ts
math.ts:
export function add(a: number, b: number) {
return a + b;
}
string.ts:
export function upper(value: string) {
return value.toUpperCase();
}
如果其他文件分别导入:
import { add } from "./utils/math";
import { upper } from "./utils/string";
文件多了以后,导入路径会比较分散。
可以在 utils/index.ts 中统一导出:
export * from "./math";
export * from "./string";
然后:
import { add, upper } from "./utils";
console.log(add(1, 2)); // 3
console.log(upper("go")); // GO
这叫作 重新导出(Re-export) 。
需要注意:
export * from "./math" 会重新导出目标模块的命名导出,但不会自动重新导出它的默认导出。
八、export { xxx } from:选择性重新导出
如果只希望重新导出某个函数:
export { add } from "./math";
还可以修改导出名称:
export { add as sum } from "./math";
如果要重新导出其他模块的默认导出:
export { default as calculate } from "./math";
它的含义是:
从 math.ts 取出默认导出,并在当前模块中以 calculate 作为命名导出重新暴露。
对应导入:
import { calculate } from "./utils";
还可以直接转发默认导出:
export { default } from "./math";
九、export * as:将整个模块作为一个对象导出
例如:
// math.ts
export function add(a: number, b: number) {
return a + b;
}
export function sub(a: number, b: number) {
return a - b;
}
在 index.ts 中:
export * as MathUtils from "./math";
导入:
import { MathUtils } from "./index";
console.log(MathUtils.add(1, 2)); // 3
console.log(MathUtils.sub(5, 2)); // 3
这种方式很像 Go 中通过包名访问函数:
mathutil.Add(1, 2)
只是 TypeScript 的 MathUtils 是由模块命名空间导出形成的对象,两者并非完全相同的机制。
十、export enum:导出枚举
TypeScript 还支持枚举。
// status.ts
export enum TaskStatus {
Pending = "pending",
Running = "running",
Success = "success"
}
导入:
import { TaskStatus } from "./status";
const status = TaskStatus.Running;
console.log(status); // running
和 Go 中使用常量定义状态值的方式有一定相似之处。
例如:
type TaskStatus string
const (
Pending TaskStatus = "pending"
Running TaskStatus = "running"
Success TaskStatus = "success"
)
需要注意:普通 TypeScript enum 通常会生成运行时 JavaScript 对象,而 type 和 interface 不会。
十一、export =:兼容 CommonJS 模块
有时查看一些较早的 TypeScript 或 Node.js 项目,会看到:
export = Logger;
例如:
// logger.ts
class Logger {
log(message: string) {
console.log(message);
}
}
export = Logger;
这是 TypeScript 为兼容 CommonJS 等模块模式提供的语法。
在兼容的 TypeScript 模块配置下,可以使用:
import Logger = require("./logger");
const logger = new Logger();
logger.log("hello");
它与现代 ES 模块中的 export default 不是同一个语法机制。
新项目通常优先选择标准的 export 和 export default,除非需要兼容已有的 CommonJS 接口。
十二、export as namespace:声明全局命名空间
在第三方库的 .d.ts 类型声明文件中,偶尔会看到:
export as namespace MyLib;
例如:
// mylib.d.ts
export function add(a: number, b: number): number;
export as namespace MyLib;
它表示这个库除模块导入方式外,还可以按特定的全局命名空间形式提供类型声明。
这常用于支持既能通过模块导入、又能通过浏览器全局变量使用的库。
但是:
export as namespace 不会创建实际的全局变量。 是否存在全局对象仍然取决于库的运行时实现。
一般开发业务代码时很少需要自己编写这种语法。
十三、export {}:把文件标记为模块
还有一种看起来很奇怪的写法:
export {};
它没有导出任何函数或变量。
那它有什么用?
作用是:告诉 TypeScript,这个文件是一个模块,而不是一个普通的全局脚本文件。
例如:
// demo.ts
const name = "张三";
export {};
这里的 name 属于当前模块内部,不会因为它是在文件顶层定义的,就被当作全局脚本声明。
在某些需要扩展全局类型的声明文件中,也会看到这种写法:
export {};
declare global {
interface Window {
appVersion: string;
}
}
declare global 用于扩展全局类型声明,而 export {} 确保这个文件被视为外部模块。
注意:这里仍然只是类型扩展,并没有实际给 window 对象赋值。
十四、declare 与 export 的本质区别
前面介绍了 export declare,这里把两者放到一起对比。
1. export function:导出实际实现
export function add(a: number, b: number): number {
return a + b;
}
这个文件提供了真实的函数实现。
2. export declare function:只导出类型声明
export declare function add(
a: number,
b: number
): number;
它不提供函数实现,只告诉 TypeScript:
这个模块应该存在一个叫 add 的函数,接收两个数字,返回一个数字。
| 对比 | export function | export declare function |
|---|---|---|
| 导出函数信息 | 是 | 是 |
| 是否包含函数实现 | 是 | 否 |
| 是否生成函数代码 | 是 | 否 |
| 常见文件 | .ts | .d.ts |
| 常见用途 | 编写业务逻辑 | 描述库的 API 类型 |
可以记住:
export解决的是:这个模块对外提供什么?declare解决的是:告诉 TypeScript 某个东西存在,它是什么类型?export declare解决的是:把某个已经存在或预期存在的 API 类型声明提供给其他模块。
十五、常见 export 语法速查表
| 语法 | 作用 |
|---|---|
export const a = 1 | 导出变量 |
export function foo() {} | 导出函数 |
export class User {} | 导出类 |
export interface User {} | 导出接口 |
export type ID = number | 导出类型别名 |
export enum Status {} | 导出枚举 |
export default foo | 默认导出 |
export { foo, bar } | 集中命名导出 |
export { foo as bar } | 重命名导出 |
export type { User } | 只导出类型 |
export * from "./utils" | 批量重新导出命名成员 |
export { foo } from "./utils" | 选择性重新导出 |
export * as Utils from "./utils" | 以命名空间形式重新导出 |
export declare function foo(): void | 导出函数类型声明,不提供实现 |
export declare const VERSION: string | 导出变量类型声明,不创建变量 |
export = foo | TypeScript 的 CommonJS 兼容导出 |
export as namespace Foo | 声明全局命名空间用法 |
export {} | 将文件标记为模块 |
十六、实际项目中应该怎么选择?
对于刚从 Go 转到 TypeScript 的开发者,可以按照下面的原则来使用:
第一种:编写普通业务函数
优先使用命名导出:
export function createTask() {}
export function deleteTask() {}
export function queryTask() {}
第二种:编写主要对外提供一个对象的文件
可以使用默认导出:
export default class TaskService {
createTask() {}
deleteTask() {}
}
第三种:定义数据结构或类型
使用 export interface 或 export type:
export interface Task {
id: number;
status: string;
}
export type TaskId = number;
第四种:为外部 JavaScript 库编写类型声明
使用 .d.ts 声明文件:
export declare function createTask(
name: string
): number;
第五种:大型项目统一管理导出
可以通过 index.ts 重新导出:
export * from "./task";
export * from "./user";
export * from "./config";
十七、总结
从 Go 后端转向 TypeScript,最重要的是理解以下几个概念:
export:把当前模块的成员暴露给其他模块。import:从其他模块中导入成员。export default:提供模块的默认导出,导入时可以自由命名。declare:只提供类型声明,不创建实际运行时代码。export declare:将某个 API 的类型声明导出给其他模块。export type:导出类型,不产生对应的运行时值。export * from:将其他模块的命名导出统一重新导出。export {}:将文件标记为模块。
最终记住一句话: export 管模块对外暴露什么, import 管模块之间怎么引用, declare 管类型系统如何认识已有的外部代码。
掌握这些以后,再阅读 TypeScript 工程里的 index.ts、types.ts、*.d.ts 等文件,就容易理解得多了。