12 KiB
TSX Refactoring with ts-morph
Automated TSX/TypeScript refactoring tools using ts-morph to extract code blocks into separate, well-typed, lint-compliant files with automatic import management. The intent is to make smaller components practical (e.g., 50/100/150 LOC thresholds) by pushing logic into hooks/utilities and iterating via a feedback loop.
Features
✨ Automated Code Extraction
- Extracts types and interfaces to separate
.types.tsfiles - Extracts utility functions to separate
.utils.tsfiles - Automatically generates and fixes imports
- Supports smaller component targets (e.g., 50/100/150 LOC) by pushing logic into hooks and helpers
- Designed for iterative runs so each pass can shrink the component further
🔍 Type Analysis & Auto-Fixing
- Infers and adds missing return types
- Adds parameter type annotations
- Replaces
anytypes with specific types - Reports type coverage metrics
🎨 Lint Auto-Fixing
- Integrates with ESLint and Prettier
- Organizes imports automatically
- Fixes common linting issues
- Ensures code style consistency
Project Structure
tsmorph/
├── src/
│ └── components/
│ ├── UserManagementDashboard.tsx # Main component (532 LOC)
│ ├── UserManagementDashboard.types.ts # Extracted types (22 LOC)
│ └── UserManagementDashboard.utils.ts # Extracted utilities (50 LOC)
├── scripts/
│ ├── refactor-tsx.ts # Main refactoring script (pass 1)
│ ├── refactor-tsx-pass2.ts # Extract inner functions (pass 2)
│ ├── analyze-types.ts # Type analysis and auto-fixing
│ └── fix-lint.ts # Lint auto-fixer
├── package.json
├── tsconfig.json
├── .eslintrc.js
└── .prettierrc
Installation
npm install
Usage
Quick Start - Full Refactoring Pipeline
# Step 1: Extract types and interfaces
npm run refactor
# Step 2: Extract utility functions
npm run refactor:pass2
# Step 3: Add missing types and improve type annotations
npm run analyze-types
# Step 4: Fix linting issues and format code
npm run fix-lint
# Step 5: Verify everything compiles
npm run type-check
Individual Commands
| Command | Description |
|---|---|
npm run refactor |
Extract types/interfaces from component |
npm run refactor:pass2 |
Extract utility functions from component |
npm run analyze-types |
Analyze and fix type annotations |
npm run fix-lint |
Auto-fix lint issues and format code |
npm run type-check |
Verify TypeScript compilation |
npm run lint |
Check for lint issues |
npm run format |
Format all files with Prettier |
CLI Options (Target Any App)
Use the scripts directly for non-demo components:
ts-node scripts/refactor-tsx.ts --file path/to/Dashboard.tsx --min-function-lines 50 --min-variable-lines 25
ts-node scripts/refactor-tsx-pass2.ts --file path/to/Dashboard.tsx --helper-pattern "^(validate|get|format|handle)"
ts-node scripts/analyze-types.ts --files path/to/Dashboard.types.ts,path/to/Dashboard.utils.ts,path/to/Dashboard.tsx
Refactoring Examples
Before: Monolithic Component
// UserManagementDashboard.tsx - Everything in one file
import React, { useState } from 'react';
interface User {
id: string;
name: string;
// ... more fields
}
interface FormData {
// ...
}
export const UserManagementDashboard: React.FC = () => {
const validateForm = (data: FormData) => {
// validation logic...
};
const getRoleBadgeColor = (role: string) => {
// helper logic...
};
// ... 500+ more lines
};
After: Refactored & Organized
UserManagementDashboard.types.ts (22 LOC)
export interface User {
id: string;
name: string;
email: string;
role: 'admin' | 'user' | 'guest';
status: 'active' | 'inactive';
createdAt: string;
}
export interface FormData {
name: string;
email: string;
role: 'admin' | 'user' | 'guest';
}
export interface ValidationErrors {
name?: string;
email?: string;
role?: string;
}
UserManagementDashboard.utils.ts (50 LOC)
import type { FormData, ValidationErrors } from './UserManagementDashboard.types';
export const validateForm = (data: FormData): ValidationErrors => {
const errors: ValidationErrors = {};
// validation logic...
return errors;
};
export const getRoleBadgeColor = (role: string): "#ff6b6b" | "#4ecdc4" | "#95a5a6" | "#7f8c8d" => {
switch (role) {
case 'admin': return '#ff6b6b';
case 'user': return '#4ecdc4';
case 'guest': return '#95a5a6';
default: return '#7f8c8d';
}
};
export const getStatusBadgeColor = (status: string): "#2ecc71" | "#e74c3c" => {
return status === 'active' ? '#2ecc71' : '#e74c3c';
};
export const formatDate = (dateString: string): string => {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
});
};
UserManagementDashboard.tsx (size varies)
import React, { useState, useEffect, useCallback } from 'react';
import { User, FormData, ValidationErrors } from './UserManagementDashboard.types';
import {
validateForm,
getRoleBadgeColor,
getStatusBadgeColor,
formatDate
} from './UserManagementDashboard.utils';
export const UserManagementDashboard: React.FC = (): React.JSX.Element => {
// Component logic...
};
How It Works
1. Type Extraction (refactor-tsx.ts)
The script analyzes the AST using ts-morph to:
- Find all interface and type alias declarations
- Extract them to a separate
.types.tsfile - Add
exportkeywords automatically - Remove them from the original file
- Add import statement to the original file
// Analyzes file structure
const interfaces = sourceFile.getInterfaces();
const typeAliases = sourceFile.getTypeAliases();
// Extracts to new file with exports
typesFile.replaceWithText(typesContent);
// Adds import to original
sourceFile.addImportDeclaration({
moduleSpecifier: './Component.types',
namedImports: typeNames,
});
2. Utility Function Extraction (refactor-tsx-pass2.ts)
Identifies and extracts helper functions:
- Finds const declarations with arrow functions inside components
- Filters by naming patterns (
validate*,get*,format*,handle*) - Exports them with proper type imports
- Updates original component with imports
// Find helper functions in component body
const helperFunctions = statements
.filter(stmt => stmt.isKind(SyntaxKind.VariableStatement))
.filter(decl => decl.name.match(/^(validate|get|format|handle)/));
// Extract and make exported
const exportedText = text.replace(/^(const|let|var)/, 'export const');
3. Type Analysis (analyze-types.ts)
Automatically improves type safety:
- Adds return types: Infers from function body
- Adds parameter types: Uses TypeScript's type inference
- Fixes 'any' types: Replaces with specific inferred types
- Ensures exports: Adds missing
exportkeywords to types - Type coverage report: Shows typed vs untyped declarations
// Infer and add return type
const returnType = func.getReturnType();
const typeText = returnType.getText(func);
func.setReturnType(typeText);
// Example output:
// ✓ Added return type to getRoleBadgeColor: "#ff6b6b" | "#4ecdc4" | "#95a5a6" | "#7f8c8d"
4. Lint Auto-Fixing (fix-lint.ts)
Ensures code quality:
- Organize imports: Sorts React first, then external, then local
- Format with Prettier: Consistent code style
- Fix with ESLint: Auto-fixes common issues
- Non-destructive: Only applies safe transformations
Type Analysis Output Example
📊 Type Analysis: UserManagementDashboard.utils.ts
✓ Added return type to validateForm: ValidationErrors
✓ Added return type to getRoleBadgeColor: "#ff6b6b" | "#4ecdc4" | "#95a5a6" | "#7f8c8d"
✓ Added return type to getStatusBadgeColor: "#2ecc71" | "#e74c3c"
✓ Added return type to formatDate: string
💾 Saved 4 type improvements
📈 Type Coverage Report
========================
UserManagementDashboard.utils.ts:
Type Coverage: 100% (4/4 declarations typed)
✅ No 'any' types found
Configuration
TypeScript Configuration (tsconfig.json)
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"esModuleInterop": true,
"jsx": "preserve",
"moduleResolution": "bundler"
}
}
ESLint Configuration (.eslintrc.js)
module.exports = {
parser: '@typescript-eslint/parser',
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'prettier',
],
rules: {
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
'@typescript-eslint/no-explicit-any': 'warn',
},
};
Prettier Configuration (.prettierrc)
{
"semi": true,
"singleQuote": true,
"printWidth": 100,
"trailingComma": "es5"
}
Benefits
✅ Maintainability
- Smaller files: Each file has a single responsibility
- Easy to locate: Types and utilities are in predictable locations
- Reduced complexity: Component focuses on UI logic
✅ Reusability
- Shared types: Can be imported across multiple components
- Utility functions: Easily tested and reused
- DRY principle: Eliminates code duplication
✅ Type Safety
- 100% type coverage: All functions have proper type annotations
- Precise types: Union types for string literals (e.g., color values)
- No 'any' types: Strong typing throughout
✅ Code Quality
- Lint-compliant: All code passes ESLint checks
- Consistent style: Prettier ensures uniform formatting
- Auto-formatted: No manual formatting needed
Metrics
| Metric | Before | After | Improvement |
|---|---|---|---|
| Main component LOC | 603 | 532 | -12% |
| Files | 1 | 3 | Organized |
| Type coverage | ~60% | 100% | +40% |
| 'any' types | Several | 0 | Eliminated |
| Imports managed | Manual | Automatic | Automated |
Advanced Usage
Customizing Extraction Patterns
Edit scripts/refactor-tsx-pass2.ts to change which functions get extracted:
// Current pattern
if (name.match(/^(validate|getRoleBadgeColor|getStatusBadgeColor|formatDate)/)) {
helperFunctions.push({ name, text });
}
// Custom pattern - extract all 'use*' hooks
if (name.match(/^use[A-Z]/)) {
helperFunctions.push({ name, text });
}
Running on Different Files
Modify the file paths in each script:
const targetFile = path.join(
__dirname,
'..',
'src',
'components',
'YourComponent.tsx' // Change this
);
Best Practices
- Run in sequence: Execute refactor → refactor:pass2 → analyze-types → fix-lint
- Review changes: Always review extracted code before committing
- Test thoroughly: Run type-check after each refactoring step
- Commit incrementally: Commit after each successful extraction
- Keep naming consistent: Use predictable naming patterns for utilities
Troubleshooting
"Cannot find name" errors after refactoring
Problem: Imports were not added correctly.
Solution:
# Re-run the refactoring scripts
npm run refactor
npm run refactor:pass2
npm run type-check
Type inference produces complex types
Problem: Auto-generated types are too long or complex.
Solution: Manually simplify the type or add explicit annotations before refactoring.
Lint issues remain after fix-lint
Problem: Some lint rules require manual fixes.
Solution:
npm run lint # Review remaining issues
npm run lint:fix # Try auto-fix again
Contributing
To add new refactoring patterns:
- Create a new script in
scripts/ - Use ts-morph to analyze and transform the AST
- Add a corresponding npm script in
package.json - Update this README with usage instructions
Dependencies
- ts-morph (^21.0.0): TypeScript compiler API wrapper
- typescript (^5.0.0): TypeScript compiler
- eslint (^8.0.0): JavaScript/TypeScript linter
- prettier (^3.0.0): Code formatter
- ts-node (^10.9.0): Execute TypeScript directly
License
MIT