第7章 模块系统
本章定位:学习TypeScript的模块系统,包括模块的基本概念、导入导出语法、模块解析策略,以及与Python import系统的对比。
7.1 模块的概念:import与export
Section titled “7.1 模块的概念:import与export”TypeScript使用ES Modules(ESM)作为标准模块系统。每个.ts文件就是一个模块。模块内定义的变量、函数、类默认是私有的,需要显式导出才能被其他模块使用。
export 导出
Section titled “export 导出”导出变量和常量:
// 导出单个export const APP_NAME = "MyApp";export const VERSION = "1.0.0";
// 先定义再导出const MAX_SIZE = 100;export { MAX_SIZE };
// 重命名导出const MAX_USERS = 50;export { MAX_USERS as MAX_USER_COUNT };
// 导出类型export type UserId = string | number;export interface User { name: string; age: number;}导出函数:
// 直接导出函数export function add(a: number, b: number): number { return a + b;}
// 先定义再导出function multiply(a: number, b: number): number { return a * b;}export { multiply };
// 重命名导出function divide(a: number, b: number): number { return a / b;}export { divide as div };导出类:
export class User { constructor( public name: string, public age: number ) {}
greet(): string { return `Hello, I'm ${this.name}`; }}
// 导出接口export interface Config { host: string; port: number;}import 导入
Section titled “import 导入”导入导出成员:
// 导入命名的导出import { add, multiply } from "./math";
console.log(add(1, 2)); // 3console.log(multiply(3, 4)); // 12
// 重命名导入import { add as sum, multiply as times } from "./math";
// 导入所有命名导出import * as MathUtils from "./math";console.log(MathUtils.add(1, 2));console.log(MathUtils.multiply(3, 4));导入默认值导出:
// 假设 utils.ts 导出了默认函数// export default function() { ... }
// 导入默认导出import myFunction from "./utils";
// 混合导入(默认 + 命名)import defaultExport, { named1, named2 } from "./module";export 与 import 的组合
Section titled “export 与 import 的组合”// 重新导出export { add, multiply } from "./math";export { User } from "./user";
// 重新导出并重命名export { add as sum } from "./math";
// 重新导出所有export * from "./math";export type { User, Product } from "./models";7.2 默认导出 vs 命名导出
Section titled “7.2 默认导出 vs 命名导出”默认导出(Default Export)
Section titled “默认导出(Default Export)”每个模块只能有一个默认导出:
export default function(a: number, b: number): number { return a + b;}
// 或者const calculator = { add: (a: number, b: number) => a + b, subtract: (a: number, b: number) => a - b};export default calculator;
// 导入(可以任意命名)import calc from "./math";calc.add(1, 2);适合默认导出的场景:
- 每个文件只导出一个主要功能
- 类(但要谨慎,因为类通常有多个实例化方式)
- 整个模块的入口
命名导出(Named Export)
Section titled “命名导出(Named Export)”可以有多个命名导出:
export const PI = 3.14159;export const E = 2.71828;
export function add(a: number, b: number): number { return a + b;}
export function subtract(a: number, b: number): number { return a - b;}
export class Calculator { add(a: number, b: number): number { return a + b; }}
// 导入import { PI, E, add, subtract, Calculator } from "./operations";| 特性 | 默认导出 | 命名导出 |
|---|---|---|
| 数量 | 每个模块一个 | 每个模块多个 |
| 导入名称 | 可任意命名 | 必须使用原名或重命名 |
| 重导出 | 需指定名称 | 简单 |
| 常用场景 | 工具函数、主类 | 常量、工具函数、多个类 |
const helper = { format: (s: string) => s.trim()};
export default helper;export const VERSION = "1.0.0";export function greet(name: string): string { return `Hello, ${name}!`;}
// 导入import helper, { VERSION, greet } from "./main";
helper.format(" hi ");console.log(VERSION);console.log(greet("Alice"));TypeScript的特殊处理
Section titled “TypeScript的特殊处理”// 当模块同时有默认导出和命名导出时// 导入方式import defaultExport, { named1, named2 } from "./module";
// 或者分别导入import * as module from "./module";module.default;module.named1;7.3 模块解析策略
Section titled “7.3 模块解析策略”相对导入 vs 非相对导入
Section titled “相对导入 vs 非相对导入”相对导入:
// 从当前目录导入import { User } from "./user";import { Config } from "./config";
// 从上级目录导入import { Helper } from "../utils/helper";
// 从上两级目录导入import { Logger } from "../../lib/logger";非相对导入:
// 从node_modules导入import express from "express";import { Component } from "react";
// 从@scope导入import { Button } from "@mui/material";模块解析算法
Section titled “模块解析算法”TypeScript使用两种模块解析策略:
Classic策略(默认,用于module: commonjs以外的情况):
导入 "module/name"1. 从当前目录向上查找 node_modules/module/name.ts2. 继续向上查找直到根目录Node策略(用于module: commonjs或moduleResolution: node):
导入 "module/name"1. 如果是".js"或".json",直接解析2. 查找 node_modules/module/package.json 的 "main" 字段3. 查找 node_modules/module/index.js4. 向上级目录继续查找tsconfig.json 中的模块配置
Section titled “tsconfig.json 中的模块配置”{ "compilerOptions": { "module": "commonjs", // 输出模块格式 "moduleResolution": "node", // 解析策略 "baseUrl": "./src", // 基础路径 "paths": { "@utils/*": ["utils/*"], "@components/*": ["components/*"] }, "rootDirs": ["src", "generated"] // 多个源目录 }}// tsconfig.json{ "compilerOptions": { "baseUrl": "./src", "paths": { "@utils/*": ["utils/*"], "@components/*": ["components/*"] } }}
// 使用import { formatDate } from "@utils/date";import { Button } from "@components/Button";// TypeScript编译器会根据以下顺序查找// 1. .ts// 2. .tsx// 3. .d.ts
// 对于JavaScript文件// 1. .js// 2. .json
// 明确扩展名import { User } from "./user.ts";7.4 命名空间(namespace)
Section titled “7.4 命名空间(namespace)”命名空间是TypeScript提供的代码组织方式,用于将相关代码分组:
namespace Validation { export interface StringValidator { isValid(s: string): boolean; }
export class EmailValidator implements StringValidator { isValid(s: string): boolean { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s); } }
export class PhoneValidator implements StringValidator { isValid(s: string): boolean { return /^\d{3}-\d{4}-\d{4}$/.test(s); } }}
// 使用const validators: Validation.StringValidator[] = [ new Validation.EmailValidator(), new Validation.PhoneValidator()];命名空间分割
Section titled “命名空间分割”可以跨文件分割命名空间:
namespace Validation { export interface Validator {}}
// email-validator.ts/// <reference path="validation.ts" />namespace Validation { export class EmailValidator implements Validator {}}
// phone-validator.ts/// <reference path="validation.ts" />namespace Validation { export class PhoneValidator implements Validator {}}命名空间 vs 模块
Section titled “命名空间 vs 模块”| 特性 | 命名空间 | 模块 |
|---|---|---|
| 文件组织 | 跨文件分组 | 一个文件一个模块 |
| 编译输出 | 单一对象 | 符合ES模块标准 |
| 导入方式 | 全局访问 | import语句 |
| 推荐场景 | 全局类型、声明 | 应用代码 |
现代TypeScript推荐:使用模块(ES Modules)而非命名空间。
// 推荐:使用模块export interface User { name: string; age: number;}
// user/service.tsimport { User } from "./types";
export function createUser(name: string, age: number): User { return { name, age };}7.5 Python import vs TypeScript import
Section titled “7.5 Python import vs TypeScript import”基础语法对比
Section titled “基础语法对比”Python import:
# 导入整个模块import os
# 导入指定成员from math import pi, e
# 重命名import pandas as pdfrom collections import OrderedDict as OD
# 相对导入from . import utilsfrom ..package import moduleTypeScript import:
// 导入整个模块import * as os from "os";
// 导入命名的导出import { pi, e } from "math";
// 重命名import { DataFrame as DF } from "pandas";
// 默认导入import React from "react";
// 相对导入import { utils } from "./utils";import { module } from "../package/module";模块路径对比
Section titled “模块路径对比”Python:
# 标准库import jsonfrom collections import OrderedDict
# 第三方库import numpy as npfrom django.http import JsonResponse
# 本地模块from .models import Userfrom ..utils import helperTypeScript:
// 标准库(JavaScript内置)import { promises as fs } from "fs";import path from "path";
// 第三方库(DefinitelyTyped类型定义)import express from "express";import { Request, Response } from "express";
// 本地模块import { User } from "./models";import { helper } from "../utils";导出语法对比
Section titled “导出语法对比”Python:
# 直接在模块中定义def func(): pass
class MyClass: pass
# __all__ 控制导出__all__ = ["func", "MyClass"]TypeScript:
export function func() {}export class MyClass {}
// 重新导出export { func as anotherName } from "./other";导入时的类型
Section titled “导入时的类型”Python:
from typing import List, Optional
def process(items: List[int]) -> Optional[str]: pass
# 类型检查由mypy等工具完成TypeScript:
import { List, Optional } from "typescript";
function process(items: number[]): string | undefined { return items.length > 0 ? "ok" : undefined;}
// 编译时类型检查默认值的处理
Section titled “默认值的处理”Python:
# 每个模块只能有一个默认导出class Utils: pass
default_app = Utils()
# main.pyfrom utils import default_app as appTypeScript:
class Utils { // ...}
export default new Utils();
// main.tsimport app from "./utils";循环导入处理
Section titled “循环导入处理”Python:
from b import B
class A: pass
# b.pyfrom a import A
class B: pass
# Python会报ImportErrorTypeScript:
import { B } from "./b";
export class A { b: B; constructor() { this.b = new B(); }}
// b.tsimport { A } from "./a";
export class B { // ...}
// TypeScript需要通过接口或延迟绑定解决7.6 三斜线指令与类型声明文件
Section titled “7.6 三斜线指令与类型声明文件”用于声明文件之间的依赖:
/// <reference path="./logger.ts" />/// <reference types="node" />常见用途:
// 1. 引用同目录下其他文件/// <reference path="./base.ts" />
// 2. 引用node_modules类型/// <reference types="express" />
// 3. 声明全局变量(使用const、///、declare)/// <reference types="google.maps" />类型声明文件(.d.ts)
Section titled “类型声明文件(.d.ts)”用于为JavaScript代码提供类型信息:
// 声明全局变量declare const APP_VERSION: string;declare const API_BASE_URL: string;
// 声明全局函数declare function formatDate(date: Date): string;
// 声明模块declare module "my-library" { export function doSomething(): void; export class MyClass { constructor(value: string); getValue(): string; }}第三方库的类型声明
Section titled “第三方库的类型声明”安装带有类型的库:
npm install @types/expressnpm install @types/node如果没有类型声明,可以创建本地声明:
declare module "my-untyped-lib" { export function process(data: string): string; export class Processor { constructor(options?: object); run(): void; }}逐步添加类型
Section titled “逐步添加类型”// 原始JavaScriptfunction greet(name) { return "Hello, " + name;}
// 1. 创建类型声明文件// greet.d.tsdeclare function greet(name: string): string;
// 2. 或者在代码中添加JSDoc注释/** * @param {string} name * @returns {string} */function greet(name) { return "Hello, " + name;}
// 3. 或者直接使用TypeScriptfunction greet(name: string): string { return `Hello, ${name}`;}7.7 模块的动态导入
Section titled “7.7 模块的动态导入”静态导入 vs 动态导入
Section titled “静态导入 vs 动态导入”静态导入(在模块顶层):
import { helper } from "./helper";
// 编译时确定,不能在条件语句中使用动态导入(按需加载):
// 动态导入模块const modulePath = condition ? "./utils" : "./helpers";
import(modulePath).then(module => { module.doSomething();});
// async/await 方式async function loadModule() { const module = await import("./utils"); return module;}动态导入的应用场景
Section titled “动态导入的应用场景”代码分割:
// 按需加载重型模块async function loadDataAnalysis() { const { DataAnalyzer } = await import("./data-analyzer"); return new DataAnalyzer();}
// 用户点击按钮时才加载button.addEventListener("click", async () => { const { Chart } = await import("./chart"); const chart = new Chart(data); chart.render();});条件加载:
const locale = getUserLocale();
const messages = await import(`./locales/${locale}.ts`);messages.default.greeting;动态导入与类型
Section titled “动态导入与类型”// 动态导入返回Promise,类型会自动推断async function loadConfig() { const { config } = await import("./config"); return config;}
// 或者显式指定类型type ConfigModule = typeof import("./config");async function loadConfig(): Promise<ConfigModule> { return import("./config");}7.8 本章小结
Section titled “7.8 本章小结”本章学习了TypeScript的模块系统:
-
导入导出基础:
- 使用
export导出,import导入 - 支持命名导出和默认导出
- 可以在导入时重命名
- 使用
-
模块解析:
- 相对导入(
./、../)和非相对导入 - Node.js风格解析策略
tsconfig.json配置路径别名
- 相对导入(
-
命名空间:
- 使用
namespace组织代码 - 跨文件分割
- 现代代码推荐使用模块替代
- 使用
-
Python对比:
- 语法类似但符号不同(
fromvs{ }) - 都支持重命名和相对导入
- Python用
.,TypeScript用/
- 语法类似但符号不同(
-
类型声明文件:
.d.ts为JavaScript提供类型信息- 使用
declare声明类型 @types/*提供第三方库类型
-
动态导入:
import()按需加载- 支持代码分割和条件加载
- 返回Promise
练习7.1:模块创建
Section titled “练习7.1:模块创建”创建以下文件:
math/utils.ts:导出add、subtract、multiply函数math/index.ts:重新导出所有函数main.ts:导入并使用
练习7.2:默认导出
Section titled “练习7.2:默认导出”创建一个config.ts,使用默认导出导出配置对象。
练习7.3:路径别名
Section titled “练习7.3:路径别名”配置tsconfig.json,设置@utils/*别名,然后在代码中使用。
练习7.4:动态导入
Section titled “练习7.4:动态导入”实现一个按需加载模块的功能,根据用户选择加载不同的计算器。
练习7.5:类型声明
Section titled “练习7.5:类型声明”为无类型的npm包创建本地类型声明文件。
下一章我们将学习TypeScript的类型守卫与类型收缩,掌握如何在运行时安全地检查类型。