TypeScript模块化开发实战指南:解决命名冲突代码复用和项目结构混乱,附企业级项目配置方案
先说说我们踩过的坑
做项目的时候,你是不是也遇到过这种让人抓狂的情况?
早上刚写完一个工具函数,下午同事过来问为什么他的模块报错了。你一脸懵逼地打开一看,好家伙,原来他也在同一个文件里定义了 getUserInfo。两个函数长得一模一样,但逻辑完全不同。最后只能苦哈哈地把其中一个改名叫 getUserInfoV2 或者 getUserInfoFromAPI。
还有那种情况:项目越做越大,文件结构像一团乱麻。今天加个组件放 src/components,明天加个工具放 src/utils,后天发现还要再加个 src/helpers。三个月后,你自己都找不到代码在哪里了。
更别提那种”我导入了这个模块,为什么还是报未定义”的灵异事件。明明在A文件里export了,在B文件里import了,跑起来却告诉你 undefined。
如果你遇到过以上任何一种情况,那你来对地方了。今天咱们不聊虚的,直接上干货,把TypeScript模块化开发里最让人头疼的几个问题一次性解决掉。
第一关:命名冲突怎么破?
问题本质
TypeScript本身是静态类型语言,编译时就能发现很多问题。但命名冲突不是TypeScript的锅,是JavaScript历史的遗留问题。
在ES6模块化出现之前,我们只能用全局变量。两个库同时定义 calculate 函数,谁先加载谁就赢,后者直接覆盖前者。这种”谁先加载谁牛”的规则,简直是一场灾难。
解决方案一:命名空间(Namespace)
TypeScript提供了一个叫 namespace 的关键字,可以把相关代码组织在一起:
// utils/calculation.ts
namespace MathUtils {
export function add(a: number, b: number): number {
return a + b;
}
export function multiply(a: number, b: number): number {
return a * b;
}
}
// 使用的地方
const result = MathUtils.add(10, 20);
听起来不错对吧?但说实话,我现在不太推荐用namespace。为什么?
因为它只是编译时的组织手段,打包后还是会变成全局变量。而且namespace之间不能互相import,只能在同一个文件里用。
解决方案二:ES6 Module(推荐)
现代TypeScript开发应该用ES6的import/export。这才是正道:
// utils/add.ts
export function add(a: number, b: number): number {
return a + b;
}
// utils/multiply.ts
export function multiply(a: number, b: number): number {
return a * b;
}
// 使用的地方
import { add } from './utils/add';
import { multiply } from './utils/multiply';
const sum = add(10, 20);
const product = multiply(10, 20);
每个文件就是一个模块,作用域天然隔离。不会有任何命名冲突,因为只有你显式导出的东西才能被访问。
解决方案三: Barrel 导出(进阶)
当模块越来越多的时候,你不想在每一行都写长长的路径。Barrel模式就是解决方案:
// utils/index.ts
export { add } from './add';
export { multiply } from './multiply';
export { subtract } from './subtract';
export { divide } from './divide';
// 使用的地方
import { add, multiply } from './utils';
这样你只需要记住 ./utils 这一层,内部的结构变化对你透明。
真实项目中的例子
让我们看一个稍微复杂点的场景:
// models/user.ts
export interface User {
id: number;
name: string;
email: string;
}
export function createUser(data: { name: string; email: string }): User {
return {
id: Math.random(),
...data,
};
}
// models/product.ts
export interface Product {
id: number;
name: string;
price: number;
}
export function createProduct(data: { name: string; price: number }): Product {
return {
id: Math.random(),
...data,
};
}
// index.ts (Barrel)
export * from './models/user';
export * from './models/product';
// app.ts
import { User, Product, createUser, createProduct } from './index';
const user = createUser({ name: '张三', email: 'zhangsan@example.com' });
const product = createProduct({ name: 'TypeScript实战', price: 99 });
看到没有?每个模型文件都是独立的模块,通过Barrel文件统一导出。使用时只需要一个import,内部结构怎么变都不影响外部代码。
第二关:代码复用怎么搞?
问题本质
代码复用的核心不是”怎么复制粘贴”,而是”怎么设计接口”。
很多项目的问题是:今天写了个请求函数,明天又写一个差不多的,后天发现三个请求函数逻辑一样,只能复制粘贴三份。维护的时候改三个地方,漏一个就出bug。
解决方案一:泛型工具函数
TypeScript的泛型是代码复用的神器:
// utils/request.ts
export async function request<T>(url: string, options?: RequestInit): Promise<T> {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json() as Promise<T>;
}
// 使用的地方
interface User {
id: number;
name: string;
}
interface Product {
id: number;
name: string;
price: number;
}
// 类型自动推断,不需要额外声明
const user = await request<User>('/api/user');
const product = await request<Product>('/api/product');
一个函数搞定所有请求,类型安全,代码复用。
解决方案二:高阶组件(HOC)
React项目中,高阶组件是复用的经典模式:
// hoc/withLoading.ts
import { useState, useEffect } from 'react';
export function withLoading<T>(
Component: React.ComponentType<T>,
promiseFactory: (props: T) => Promise<any>
) {
return function WithLoadingComponent(props: T) {
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
setLoading(true);
promiseFactory(props)
.then(() => setLoading(false))
.catch(err => {
setError(err);
setLoading(false);
});
}, [props]);
if (error) return <div>Error: {error.message}</div>;
if (loading) return <div>Loading...</div>;
return <Component {...props} />;
};
}
// 使用的地方
const AsyncUserList = withLoading(
UserList,
() => fetch('/api/users').then(r => r.json())
);
一个HOC搞定所有需要loading状态的组件,核心逻辑只写一次。
解决方案三:自定义Hook
Vue和React都支持自定义Hook,这是比HOC更优雅的重用方式:
// hooks/usePagination.ts
import { useState, useMemo } from 'react';
export function usePagination<T>(data: T[], pageSize: number = 10) {
const [currentPage, setCurrentPage] = useState(1);
const totalPages = useMemo(() => Math.ceil(data.length / pageSize), [data, pageSize]);
const paginatedData = useMemo(() => {
const start = (currentPage - 1) * pageSize;
return data.slice(start, start + pageSize);
}, [data, currentPage, pageSize]);
const goToPage = (page: number) => {
if (page >= 1 && page <= totalPages) {
setCurrentPage(page);
}
};
return {
currentPage,
totalPages,
paginatedData,
goToPage,
isFirstPage: currentPage === 1,
isLastPage: currentPage === totalPages,
};
}
// 使用的地方
function UserList() {
const { data } = useFetch('/api/users');
const { paginatedData, goToPage, totalPages } = usePagination(data);
return (
<div>
{paginatedData.map(user => <UserCard key={user.id} user={user} />)}
<Pagination currentPage={currentPage} totalPages={totalPages} goToPage={goToPage} />
</div>
);
}
分页逻辑抽象成Hook,任何列表页面都能复用,而且类型完全安全。
完整的复用架构示例
让我们看一个稍微复杂点的场景:
// types/index.ts
export interface ApiResponse<T> {
code: number;
message: string;
data: T;
}
export interface PaginatedResponse<T> extends ApiResponse<T> {
total: number;
page: number;
pageSize: number;
}
// hooks/useRequest.ts
import { useState, useEffect } from 'react';
export function useRequest<T>(url: string, options?: RequestInit) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
setLoading(true);
fetch(url, options)
.then(res => res.json())
.then(json => {
setData(json.data);
setError(null);
})
.catch(err => setError(err))
.finally(() => setLoading(false));
}, [url, options]);
return { data, loading, error };
}
// hooks/usePagination.ts
import { useState, useMemo } from 'react';
export function usePagination<T>(data: T[], pageSize: number = 10) {
const [currentPage, setCurrentPage] = useState(1);
const totalPages = useMemo(() => Math.ceil(data.length / pageSize), [data, pageSize]);
const paginatedData = useMemo(() => {
const start = (currentPage - 1) * pageSize;
return data.slice(start, start + pageSize);
}, [data, currentPage, pageSize]);
const goToPage = (page: number) => {
if (page >= 1 && page <= totalPages) {
setCurrentPage(page);
}
};
return { currentPage, totalPages, paginatedData, goToPage };
}
// 使用的地方
function UserList() {
const { data: users, loading, error } = useRequest<ApiResponse<User[]>>(
'/api/users',
{ headers: { 'Authorization': 'Bearer token' } }
);
const { paginatedData, goToPage, totalPages } = usePagination(users?.data || [], 10);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
{paginatedData.map(user => <UserCard key={user.id} user={user} />)}
<button onClick={() => goToPage(currentPage - 1)} disabled={currentPage === 1}>
Previous
</button>
<span>Page {currentPage} of {totalPages}</span>
<button onClick={() => goToPage(currentPage + 1)} disabled={currentPage === totalPages}>
Next
</button>
</div>
);
}
这套架构的特点是:类型定义集中管理,复用逻辑抽象成Hook,使用时只关心业务逻辑。
第三关:项目结构怎么设计?
问题本质
项目结构混乱的根本原因不是”不知道放哪里”,而是”没有统一的标准”。
很多团队的问题是:新人来了不知道代码放哪里,老员工离职后代码变成天书。今天张三说放 src/components,明天李四说放 src/views,后天王五说放 src/pages。三个月后,三个文件夹里都是组件,根本分不清谁是谁。
方案一:按功能模块组织(Feature-based)
适合中小型项目,每个功能模块一个文件夹:
src/
├── features/
│ ├── user/
│ │ ├── components/
│ │ │ ├── UserCard.tsx
│ │ │ └── UserList.tsx
│ │ ├── hooks/
│ │ │ └── useUser.ts
│ │ ├── services/
│ │ │ └── userService.ts
│ │ ├── types/
│ │ │ └── user.ts
│ │ └── index.ts
│ ├── product/
│ │ ├── components/
│ │ ├── hooks/
│ │ ├── services/
│ │ └── types/
│ └── order/
│ ├── components/
│ ├── hooks/
│ ├── services/
│ └── types/
├── shared/
│ ├── components/
│ │ ├── Button.tsx
│ │ └── Input.tsx
│ ├── hooks/
│ ├── utils/
│ └── types/
└── app.tsx
每个功能模块自己管好自己,依赖关系清晰。user 模块可以独立测试,不需要知道 product 模块的存在。
方案二:按技术层级组织(Layer-based)
适合大型项目,代码按职责分层:
src/
├── components/
│ ├── common/
│ │ ├── Button.tsx
│ │ └── Input.tsx
│ └── features/
│ ├── user/
│ └── product/
├── pages/
│ ├── Home.tsx
│ ├── UserDetail.tsx
│ └── ProductList.tsx
├── hooks/
│ ├── useRequest.ts
│ └── usePagination.ts
├── services/
│ ├── api.ts
│ └── user.ts
├── store/
│ ├── userStore.ts
│ └── productStore.ts
├── types/
│ ├── user.ts
│ └── product.ts
└── utils/
├── request.ts
└── format.ts
这种结构的特点是:相同类型的代码放在一起,但需要明确的命名规范,否则容易变成”什么都放一起的大杂烩”。
方案三:混合模式(推荐)
实际项目中,我推荐混合模式:功能模块为主,技术层级为辅:
src/
├── features/
│ ├── auth/
│ │ ├── components/
│ │ ├── hooks/
│ │ ├── services/
│ │ └── types/
│ ├── user/
│ │ ├── components/
│ │ ├── hooks/
│ │ ├── services/
│ │ └── types/
│ └── product/
│ ├── components/
│ ├── hooks/
│ ├── services/
│ └── types/
├── shared/
│ ├── components/
│ │ ├── Button.tsx
│ │ └── Modal.tsx
│ ├── hooks/
│ ├── utils/
│ └── types/
├── app.tsx
└── main.tsx
features 文件夹放业务模块,shared 文件夹放通用代码。这样既保证了功能内聚,又避免了代码重复。
企业级项目配置方案
完整的 tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "node",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"jsx": "react-jsx",
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noEmit": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"isolatedModules": true,
"skipLibCheck": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@features/*": ["src/features/*"],
"@shared/*": ["src/shared/*"],
"@utils/*": ["src/shared/utils/*"],
"@hooks/*": ["src/shared/hooks/*"],
"@components/*": ["src/shared/components/*"]
}
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}
关键点:
strict: true开启所有严格检查noUnusedLocals和noUnusedParameters防止遗留无用代码paths配置路径别名,让import更简洁
Package.json 配置
{
"name": "typescript-module-demo",
"version": "1.0.0",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint src --ext .ts,.tsx",
"type-check": "tsc --noEmit",
"test": "vitest"
},
"devDependencies": {
"typescript": "^5.0.0",
"vite": "^4.0.0",
"@vitejs/plugin-react": "^4.0.0",
"eslint": "^8.0.0",
"@typescript-eslint/parser": "^5.0.0",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"vitest": "^0.30.0"
},
"dependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
}
ESLint 配置
// eslint.config.js
import tsParser from '@typescript-eslint/parser';
import tsPlugin from '@typescript-eslint/eslint-plugin';
export default [
{
files: ['**/*.ts', '**/*.tsx'],
languageOptions: {
parser: tsParser,
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module'
}
},
plugins: {
'@typescript-eslint': tsPlugin
},
rules: {
'@typescript-eslint/no-unused-vars': 'error',
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'no-console': 'warn'
}
}
];
Vite 配置
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { resolve } from 'path';
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
'@features': resolve(__dirname, 'src/features'),
'@shared': resolve(__dirname, 'src/shared'),
'@utils': resolve(__dirname, 'src/shared/utils'),
'@hooks': resolve(__dirname, 'src/shared/hooks'),
'@components': resolve(__dirname, 'src/shared/components')
}
}
});
这样在代码里就可以这样写:
// 以前
import { request } from '../../shared/utils/request';
// 现在
import { request } from '@utils/request';
简洁多了,而且不怕路径层级变化。
实战:一个完整的用户管理模块
让我们把所有知识点串起来,做一个真实的项目模块:
src/features/user/
├── index.ts
├── types/
│ └── user.ts
├── services/
│ └── userService.ts
├── hooks/
│ └── useUser.ts
├── components/
│ ├── UserCard.tsx
│ └── UserList.tsx
└── pages/
└── UserManagement.tsx
类型定义
// src/features/user/types/user.ts
export interface User {
id: string;
name: string;
email: string;
role: 'admin' | 'user';
createdAt: Date;
}
export interface CreateUserRequest {
name: string;
email: string;
role: 'admin' | 'user';
}
export interface UpdateUserRequest {
name?: string;
email?: string;
role?: 'admin' | 'user';
}
export interface UserListResponse {
users: User[];
total: number;
page: number;
pageSize: number;
}
服务层
// src/features/user/services/userService.ts
import { request } from '@utils/request';
import type { User, CreateUserRequest, UpdateUserRequest, UserListResponse } from '../types/user';
const USER_API = '/api/users';
export const userService = {
// 获取用户列表
list: (params: { page?: number; pageSize?: number }) => {
return request<UserListResponse>(USER_API, {
params
});
},
// 获取单个用户
getById: (id: string) => {
return request<User>(`${USER_API}/${id}`);
},
// 创建用户
create: (data: CreateUserRequest) => {
return request<User>(USER_API, {
method: 'POST',
body: JSON.stringify(data)
});
},
// 更新用户
update: (id: string, data: UpdateUserRequest) => {
return request<User>(`${USER_API}/${id}`, {
method: 'PUT',
body: JSON.stringify(data)
});
},
// 删除用户
remove: (id: string) => {
return request<void>(`${USER_API}/${id}`, {
method: 'DELETE'
});
}
};
Hook 层
// src/features/user/hooks/useUser.ts
import { useState, useEffect, useCallback } from 'react';
import { userService } from '../services/userService';
import type { User, CreateUserRequest, UpdateUserRequest } from '../types/user';
export function useUserList(params?: { page?: number; pageSize?: number }) {
const [users, setUsers] = useState<User[]>([]);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const fetchUsers = useCallback(async () => {
setLoading(true);
setError(null);
try {
const response = await userService.list(params);
setUsers(response.data.users);
setTotal(response.data.total);
} catch (err) {
setError(err as Error);
} finally {
setLoading(false);
}
}, [params]);
useEffect(() => {
fetchUsers();
}, [fetchUsers]);
const createUser = async (data: CreateUserRequest) => {
return userService.create(data);
};
const updateUser = async (id: string, data: UpdateUserRequest) => {
return userService.update(id, data);
};
const deleteUser = async (id: string) => {
return userService.remove(id);
};
return {
users,
total,
loading,
error,
refresh: fetchUsers,
createUser,
updateUser,
deleteUser
};
}
组件层
// src/features/user/components/UserCard.tsx
import React from 'react';
import type { User } from '../types/user';
interface UserCardProps {
user: User;
onEdit: (user: User) => void;
onDelete: (id: string) => void;
}
export const UserCard: React.FC<UserCardProps> = ({ user, onEdit, onDelete }) => {
return (
<div className="user-card">
<h3>{user.name}</h3>
<p>{user.email}</p>
<span className={`role-${user.role}`}>{user.role}</span>
<div className="actions">
<button onClick={() => onEdit(user)}>编辑</button>
<button onClick={() => onDelete(user.id)}>删除</button>
</div>
</div>
);
};
// src/features/user/components/UserList.tsx
import React from 'react';
import { UserCard } from './UserCard';
import type { User } from '../types/user';
interface UserListProps {
users: User[];
loading: boolean;
onEdit: (user: User) => void;
onDelete: (id: string) => void;
}
export const UserList: React.FC<UserListProps> = ({
users,
loading,
onEdit,
onDelete
}) => {
if (loading) return <div>加载中...</div>;
return (
<div className="user-list">
{users.map(user => (
<UserCard
key={user.id}
user={user}
onEdit={onEdit}
onDelete={onDelete}
/>
))}
</div>
);
};
页面层
// src/features/user/pages/UserManagement.tsx
import React, { useState } from 'react';
import { useUserList } from '../hooks/useUser';
import { UserList } from '../components/UserList';
import type { User } from '../types/user';
export const UserManagement: React.FC = () => {
const {
users,
total,
loading,
error,
refresh,
createUser,
updateUser,
deleteUser
} = useUserList({ page: 1, pageSize: 10 });
const [showModal, setShowModal] = useState(false);
const [editingUser, setEditingUser] = useState<User | null>(null);
const handleEdit = (user: User) => {
setEditingUser(user);
setShowModal(true);
};
const handleDelete = async (id: string) => {
if (confirm('确定要删除这个用户吗?')) {
await deleteUser(id);
refresh();
}
};
const handleCreate = async (data: any) => {
await createUser(data);
refresh();
setShowModal(false);
};
const handleUpdate = async (id: string, data: any) => {
await updateUser(id, data);
refresh();
setShowModal(false);
};
return (
<div className="user-management">
<h1>用户管理</h1>
<button onClick={() => setShowModal(true)}>新建用户</button>
{error && <div className="error">加载失败: {error.message}</div>}
<UserList
users={users}
loading={loading}
onEdit={handleEdit}
onDelete={handleDelete}
/>
{/* 这里可以放 Modal 组件 */}
</div>
);
};
模块导出
// src/features/user/index.ts
export { UserManagement } from './pages/UserManagement';
export { UserList } from './components/UserList';
export { UserCard } from './components/UserCard';
export { useUserList } from './hooks/useUser';
export type { User, CreateUserRequest, UpdateUserRequest } from './types/user';
这样外部只需要一个import就能拿到所有东西:
// src/app.tsx
import { UserManagement } from '@features/user';
function App() {
return <UserManagement />;
}
最佳实践总结
1. 模块边界要清晰
每个功能模块应该自己管好自己:类型、服务、Hook、组件。外部只需要知道模块名,不需要关心内部结构。
// 好的做法
import { UserManagement } from '@features/user';
// 不好的做法
import { UserManagement } from '@features/user/pages/UserManagement';
import { useUserList } from '@features/user/hooks/useUser';
import type { User } from '@features/user/types/user';
2. 避免循环依赖
循环依赖是模块化开发的大忌。如果A依赖B,B依赖A,要么重构,要么用依赖注入。
// 有问题的代码
// user.ts
import { authService } from './authService';
export const userService = { ... };
// authService.ts
import { userService } from './user';
export const authService = { ... };
// 解决方案:提取公共类型到独立文件
// types/user.ts
export interface User { ... }
// types/auth.ts
export interface Auth { ... }
// user.ts
import { User } from './types/user';
import { authService } from './authService';
// authService.ts
import { Auth } from './types/auth';
import { userService } from './user';
3. 统一导出入口
每个模块都应该有一个 index.ts 作为导出入口,这样外部引用时路径简洁。
4. 类型优先
先写类型,再写实现。类型是接口,实现是细节。类型定了,接口就清晰了。
5. 测试驱动
每个模块都应该有对应的测试。功能模块、Hook、工具函数,都能独立测试。
// src/features/user/hooks/__tests__/useUser.test.ts
import { renderHook, act } from '@testing-library/react';
import { useUserList } from '../useUser';
import { userService } from '../../services/userService';
vi.mock('../../services/userService');
describe('useUserList', () => {
it('应该加载用户列表', async () => {
const mockUsers = [
{ id: '1', name: '张三', email: 'zhangsan@example.com' }
];
vi.mocked(userService.list).mockResolvedValue({
data: { users: mockUsers, total: 1, page: 1, pageSize: 10 }
});
const { result, waitFor } = renderHook(() => useUserList());
await waitFor(() => expect(result.current.loading).toBe(false));
expect(result.current.users).toEqual(mockUsers);
expect(result.current.total).toBe(1);
});
});
最后的话
模块化开发不是魔法,是一套工程实践的积累。
命名冲突用ES6 Module解决,代码复用靠泛型和高阶组件,项目结构按功能模块组织。把这些习惯养成,你的代码会越来越清晰,越来越容易维护。
记住一点:好的架构不是设计出来的,是迭代出来的。先让代码跑起来,再慢慢优化结构。别追求一步到位,那只会让你一直停在原地。
希望这篇指南能帮到你。如果还有问题,欢迎评论区交流。代码路上的坑,我们一起填。
