Module best practices
TypeScript's module system provides powerful tools for code organization. Proper use of modules can significantly improve a project's maintainability and scalability. Below, we explore core modularization practices from multiple dimensions.
Module Division Principles
Module division should follow the principle of high cohesion and low coupling. A common mistake is cramming unrelated functionalities into the same module:
// Bad example: Utility functions mixed with business logic
export function formatDate(date: Date): string {
return date.toISOString()
}
export class UserService {
async login() { /*...*/ }
}
The improved approach involves splitting by functional boundaries:
// utils/date.ts
export function formatDate(date: Date): string {
return date.toISOString()
}
// services/user.ts
export class UserService {
async login() { /*...*/ }
}
Export Strategy Optimization
Avoid default exports for three reasons:
- Renaming can lead to ambiguity
- Auto-import tools may struggle to identify them accurately
- Refactoring can easily break references
Prefer named exports:
// Recommended approach
export const API_TIMEOUT = 5000
export interface UserProfile { /*...*/ }
export function fetchUser() { /*...*/ }
// Explicit import paths in usage
import { API_TIMEOUT, fetchUser } from './api'
Circular Dependency Handling
TypeScript has limited detection for circular dependencies. Avoid them by:
- Extracting common types to standalone modules
- Using dependency injection patterns
- Implementing lazy loading
// Solution example: Dependency injection
class AuthService {
constructor(private userService: UserService) {}
}
class UserService {
private authService?: AuthService
setAuthService(service: AuthService) {
this.authService = service
}
}
Path Alias Configuration
In large projects, relative paths can cause confusion. Configure tsconfig.json
:
{
"compilerOptions": {
"baseUrl": "./src",
"paths": {
"@utils/*": ["utils/*"],
"@models/*": ["models/*"]
}
}
}
Usage example:
import { formatDate } from '@utils/date'
import type { User } from '@models/user'
Dynamic Import Practices
Lazy loading modules significantly improves application performance:
// Dynamically load a rich-text editor
async function loadEditor() {
const { MarkdownEditor } = await import('@components/editor')
return MarkdownEditor
}
// Using with React Suspense
const Editor = React.lazy(() => import('@components/editor'))
Type Export Standards
Modules should explicitly export type definitions to avoid implicit type leakage:
// Correct approach: Explicit type exports
export interface Config {
env: 'dev' | 'prod'
debug: boolean
}
export function initApp(config: Config) { /*...*/ }
// Bad practice: Implicit parameter type exposure
export function initApp(config: { env: string }) { /*...*/ }
Module Testing Strategy
Maintain independence when writing tests for modules:
// Test example using Jest
import { formatDate } from '@utils/date'
describe('date utils', () => {
it('should format ISO string', () => {
const date = new Date('2023-01-01')
expect(formatDate(date)).toMatch(/2023-01-01/)
})
})
Module Documentation Standards
Enhance type hints with JSDoc:
/**
* User permission verification module
* @module services/auth
*/
/**
* Checks current user permissions
* @param {string} permission - Permission code
* @returns {Promise<boolean>} Whether permission is granted
*/
export async function checkPermission(permission: string): Promise<boolean> {
// ...
}
Module Version Management
Implement version control for public modules:
- Use
changesets
for changelog management - Follow semantic versioning
- Develop private modules using
npm link
# Typical workflow
npx changeset add
npx changeset version
npm publish
Module Performance Optimization
Analyze module size with bundling tools:
// vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer'
export default defineConfig({
plugins: [
visualizer({
open: true,
gzipSize: true
})
]
})
Hot Module Reload Configuration
Optimize HMR experience in development:
// Custom HMR logic
if (import.meta.hot) {
import.meta.hot.accept('./module', (newModule) => {
newModule?.applyUpdate()
})
}
Cross-Platform Module Design
Consider multi-platform support when designing reusable modules:
// Platform abstraction layer design
export abstract class Storage {
abstract getItem(key: string): string | null
}
// Web implementation
export class WebStorage extends Storage {
getItem(key: string) {
return localStorage.getItem(key)
}
}
// Node implementation
export class ServerStorage extends Storage {
getItem(key: string) {
return process.env[key] ?? null
}
}
Module Error Handling
Unified error handling mechanism example:
// error.ts module
export class AppError extends Error {
constructor(
public readonly code: string,
message?: string
) {
super(message)
}
}
export function handleError(error: unknown) {
if (error instanceof AppError) {
console.error(`[${error.code}]`, error.message)
} else {
console.error('Unknown error', error)
}
}
Module Lifecycle Management
Modules requiring initialization should provide clear lifecycle methods:
// db.ts module
let connection: DatabaseConnection | null = null
export async function connectDB(config: DBConfig) {
if (connection) return connection
connection = await createConnection(config)
return connection
}
export async function disconnectDB() {
if (connection) {
await connection.close()
connection = null
}
}
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn