Skill v1.0.1
currentAutomated scan100/1003 files
version: "1.0.1" name: typescript-dry-principle description: Apply DRY principle to eliminate code duplication in TypeScript projects with comprehensive refactoring patterns license: Apache-2.0 compatibility: opencode metadata: audience: developers workflow: code-refactoring
What I do
I help you eliminate code duplication in TypeScript projects by applying the DRY (Don't Repeat Yourself) principle:
- Analyze Codebase: Scan TypeScript files to identify repeated code patterns, logic, types, and configurations
- Identify Duplication Patterns: Detect common anti-patterns including:
- Duplicate logic across multiple functions/components
- Repeated type definitions
- Copy-pasted code blocks
- Similar functions with slight variations
- Scattered configuration values
- Extract Common Logic: Refactor duplicated code into reusable utility functions and modules
- Consolidate Type Definitions: Merge duplicate types into shared interfaces and type utilities
- Create Generic Solutions: Build type-safe reusable components using TypeScript generics
- Organize Folder Structure: Restructure code into logical directories (types/, utils/, constants/, hooks/, services/)
- Replace Duplicated Code: Update files to import from shared modules instead of duplicating code
- Verify Refactoring: Ensure code compiles and tests pass after changes
When to use me
Use this workflow when:
- You notice similar code blocks across multiple TypeScript files
- You're copy-pasting code between modules or components
- Type definitions are duplicated or repeated across files
- Business logic appears in multiple places with slight variations
- Configuration values are scattered across multiple files
- Tests contain repeated setup/teardown logic
- You want to improve code maintainability and reduce technical debt
- Preparing for a code review to address technical debt
- Setting up a new TypeScript project with proper code organization
Ask clarifying questions if the scope of refactoring is unclear or if you want to focus on specific areas.
Prerequisites
- TypeScript project with source code (.ts, .tsx files)
- File permissions to read and modify TypeScript files
- TypeScript compiler installed and configured
- (Optional) Test suite to verify refactoring doesn't break functionality
- (Optional) Git repository to commit refactoring changes
Steps
Step 1: Analyze Codebase for Duplication Patterns
Scan TypeScript files to identify duplication:
# Find TypeScript filesfind . -name "*.ts" -o -name "*.tsx" -not -path "*/node_modules/*" -not -path "*/dist/*" -not -path "*/build/*"# Analyze files for common patterns# Look for:# - Similar function names with variations (getUserData, getUserInfo, getUserDetails)# - Repeated API calls or data fetching logic# - Duplicate type definitions across files# - Similar component structures with slight differences
Common Duplication Indicators:
- Functions with similar names (getUser vs getUserData vs getUserInfo)
- Nearly identical code blocks with slight variations
- Same type interfaces defined in multiple files
- Repeated validation or transformation logic
- Similar component structures with different props
Step 2: Categorize Duplication Types
Identify the type of duplication to apply appropriate refactoring pattern:
| Duplication Type | Description | Refactoring Approach | |
|---|---|---|---|
| Logic Duplication | Same business logic in multiple functions | Extract to shared utility functions | |
| Type Duplication | Duplicate interfaces/types across files | Consolidate into shared types/ directory | |
| Component Duplication | Similar components with minor variations | Create generic components using TypeScript generics | |
| Configuration Duplication | Same config values in multiple files | Create constants/ directory | |
| API Call Duplication | Repeated API calls with similar logic | Create API service layer | |
| Validation Duplication | Same validation logic in multiple places | Create shared validators | |
| Template Duplication | Similar code patterns that could be templated | Create higher-order functions or components |
Step 3: Extract Common Logic to Utility Functions
Refactor duplicate logic into shared utility functions:
Example 1: Data Transformation Logic
Before (duplicated across multiple files):
// In file1.tsfunction formatUserName(firstName: string, lastName: string): string {return `${firstName.charAt(0).toUpperCase()}${firstName.slice(1).toLowerCase()} ${lastName.charAt(0).toUpperCase()}${lastName.slice(1).toLowerCase()}`}// In file2.tsfunction formatAuthorName(firstName: string, lastName: string): string {return `${firstName.charAt(0).toUpperCase()}${firstName.slice(1).toLowerCase()} ${lastName.charAt(0).toUpperCase()}${lastName.slice(1).toLowerCase()}`}
After (refactored to shared utility):
// In utils/stringUtils.tsexport function capitalizeFirstLetter(word: string): string {if (!word) return ''return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()}export function formatFullName(firstName: string, lastName: string): string {return `${capitalizeFirstLetter(firstName)} ${capitalizeFirstLetter(lastName)}`}// In file1.tsimport { formatFullName } from '../utils/stringUtils'function formatUserName(firstName: string, lastName: string): string {return formatFullName(firstName, lastName)}// In file2.tsimport { formatFullName } from '../utils/stringUtils'function formatAuthorName(firstName: string, lastName: string): string {return formatFullName(firstName, lastName)}
Example 2: API Call Duplication
Before (duplicated across multiple components):
// In component1.tsasync function fetchUser(id: string): Promise<User> {const response = await fetch(`/api/users/${id}`)if (!response.ok) throw new Error('Failed to fetch user')return response.json()}// In component2.tsasync function fetchUserProfile(id: string): Promise<UserProfile> {const response = await fetch(`/api/users/${id}/profile`)if (!response.ok) throw new Error('Failed to fetch user profile')return response.json()}
After (refactored to shared service):
// In services/apiService.tsclass ApiService {private baseUrl: string = '/api'async fetch<T>(endpoint: string): Promise<T> {const response = await fetch(`${this.baseUrl}${endpoint}`)if (!response.ok) throw new Error(`Failed to fetch ${endpoint}`)return response.json()}async getUser(id: string): Promise<User> {return this.fetch<User>(`/users/${id}`)}async getUserProfile(id: string): Promise<UserProfile> {return this.fetch<UserProfile>(`/users/${id}/profile`)}}export const apiService = new ApiService()// In component1.tsimport { apiService } from '../services/apiService'async function fetchUser(id: string): Promise<User> {return apiService.getUser(id)}// In component2.tsimport { apiService } from '../services/apiService'async function fetchUserProfile(id: string): Promise<UserProfile> {return apiService.getUserProfile(id)}
Step 4: Consolidate Type Definitions
Merge duplicate type definitions into shared interfaces:
Before (duplicated types in multiple files):
// In file1.tsinterface UserData {id: stringname: stringemail: string}// In file2.tsinterface UserInfo {id: stringfullName: stringemailAddress: string}// In file3.tsinterface UserProfile {userId: stringdisplayName: stringcontactEmail: string}
After (consolidated in shared types file):
// In types/user.tsexport interface User {id: stringname: stringemail: string}// In file1.tsimport type { User } from '../types/user'const userData: User = { /* ... */ }// In file2.tsimport type { User } from '../types/user'const userInfo: User = { /* ... */ }// In file3.tsimport type { User } from '../types/user'const userProfile: User = { /* ... */ }
Advanced Type Consolidation (using utility types):
// In types/api.tsexport type ApiResponse<T> = {data: Terror: string | nullstatus: 'success' | 'error'}export type PaginatedResponse<T> = {items: T[]total: numberpage: numberpageSize: number}// In types/common.tsexport type Optional<T> = T | null | undefinedexport type Nullable<T> = T | nullexport type DeepPartial<T> = {[P in keyof T]?: T[P]}
Step 5: Create Generic Components with TypeScript Generics
Refactor similar components into generic reusable components:
Example 1: Generic List Component
Before (duplicated list components):
// In UserList.tsxinterface UserListProps {users: User[]onSelectUser: (user: User) => void}export function UserList({ users, onSelectUser }: UserListProps) {return (<ul>{users.map(user => (<li key={user.id} onClick={() => onSelectUser(user)}>{user.name}</li>))}</ul>)}// In ProductList.tsxinterface ProductListProps {products: Product[]onSelectProduct: (product: Product) => void}export function ProductList({ products, onSelectProduct }: ProductListProps) {return (<ul>{products.map(product => (<li key={product.id} onClick={() => onSelectProduct(product)}>{product.name}</li>))}</ul>)}
After (refactored to generic component):
// In components/GenericList.tsxinterface GenericListProps<T> {items: T[]key: keyof TrenderItem: (item: T) => React.ReactNodeonSelectItem: (item: T) => void}export function GenericList<T>({ items, key, renderItem, onSelectItem }: GenericListProps<T>) {return (<ul>{items.map(item => (<li key={String(item[key])} onClick={() => onSelectItem(item)}>{renderItem(item)}</li>))}</ul>)}// In UserList.tsximport { GenericList } from './GenericList'import type { User } from '../types/user'export function UserList({ users, onSelectUser }: { users: User[]; onSelectUser: (user: User) => void }) {return (<GenericListitems={users}key="id"renderItem={(user) => <span>{user.name}</span>}onSelectItem={onSelectUser}/>)}// In ProductList.tsximport { GenericList } from './GenericList'import type { Product } from '../types/product'export function ProductList({ products, onSelectProduct }: { products: Product[]; onSelectProduct: (product: Product) => void }) {return (<GenericListitems={products}key="id"renderItem={(product) => <span>{product.name}</span>}onSelectItem={onSelectProduct}/>)}
Example 2: Generic Data Fetching Hook
Before (duplicated fetching logic in multiple components):
// In useUserData.tsexport function useUserData(userId: string) {const [data, setData] = useState<User | null>(null)const [loading, setLoading] = useState(false)const [error, setError] = useState<string | null>(null)useEffect(() => {async function fetchData() {setLoading(true)setError(null)try {const response = await fetch(`/api/users/${userId}`)const result = await response.json()setData(result)} catch (err) {setError('Failed to fetch user')} finally {setLoading(false)}}fetchData()}, [userId])return { data, loading, error }}
After (refactored to generic hook):
// In hooks/useApiData.tsinterface UseApiDataOptions {immediate?: boolean}export function useApiData<T>(url: string, options: UseApiDataOptions = {}) {const [data, setData] = useState<T | null>(null)const [loading, setLoading] = useState(false)const [error, setError] = useState<string | null>(null)useEffect(() => {async function fetchData() {setLoading(true)setError(null)try {const response = await fetch(url)const result = await response.json()setData(result)} catch (err) {setError('Failed to fetch data')} finally {setLoading(false)}}if (options.immediate !== false) {fetchData()}}, [url, options.immediate])return { data, loading, error, refetch: () => fetchData() }}// In useUserData.tsexport function useUserData(userId: string) {return useApiData<User>(`/api/users/${userId}`)}
Step 6: Create Constants Directory
Extract scattered configuration values into shared constants:
Before (configuration scattered across files):
// In component1.tsxconst API_BASE_URL = 'https://api.example.com/v1'const MAX_RETRIES = 3const TIMEOUT = 5000// In component2.tsxconst API_BASE_URL = 'https://api.example.com/v1'const MAX_RETRIES = 3const TIMEOUT = 5000// In service.tsconst API_BASE_URL = 'https://api.example.com/v1'const MAX_RETRIES = 3const TIMEOUT = 5000
After (consolidated in constants):**
// In constants/api.tsexport const API_CONFIG = {BASE_URL: 'https://api.example.com/v1',MAX_RETRIES: 3,TIMEOUT: 5000,ENDPOINTS: {USERS: '/users',PRODUCTS: '/products',ORDERS: '/orders'} as const} as constexport const UI_CONFIG = {ANIMATION_DURATION: 300,DEBOUNCE_DELAY: 500,TOAST_DURATION: 3000} as const// In component1.tsximport { API_CONFIG } from '../constants/api'async function fetchData() {const response = await fetch(`${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.USERS}`)// ...}// In component2.tsximport { API_CONFIG } from '../constants/api'async function fetchData() {const response = await fetch(`${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.PRODUCTS}`)// ...}
Step 7: Create Validator Utilities
Extract duplicate validation logic into shared validators:
Example: Email Validation
Before (duplicate validation in multiple places):
// In component1.tsxfunction validateEmail(email: string): boolean {const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/return emailRegex.test(email)}// In component2.tsxfunction checkEmail(email: string): boolean {const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/return emailPattern.test(email)}// In form.tsxfunction isEmailValid(email: string): boolean {const pattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/return pattern.test(email)}
After (consolidated in validators):**
// In utils/validators.tsexport class Validators {private static emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/static email(email: string): boolean {return this.emailRegex.test(email)}static minLength(value: string, min: number): boolean {return value.length >= min}static maxLength(value: string, max: number): boolean {return value.length <= max}static required(value: string): boolean {return value.trim().length > 0}static pattern(value: string, pattern: RegExp): boolean {return pattern.test(value)}static phone(value: string): boolean {const phoneRegex = /^\+?[\d\s-()]+$/return phoneRegex.test(value)}}// In component1.tsximport { Validators } from '../utils/validators'function validateEmail(email: string): boolean {return Validators.email(email)}// In form.tsximport { Validators } from '../utils/validators'function checkEmail(email: string): boolean {return Validators.email(email)}
Step 8: Organize Folder Structure
Restructure code into logical directories:
src/├── types/ # Shared type definitions│ ├── index.ts # Type exports│ ├── user.ts # User-related types│ ├── api.ts # API response types│ └── common.ts # Common utility types├── utils/ # Reusable utility functions│ ├── stringUtils.ts # String manipulation│ ├── dateUtils.ts # Date formatting│ ├── validators.ts # Validation logic│ └── apiHelpers.ts # API helper functions├── constants/ # Configuration values│ ├── api.ts # API endpoints and config│ └── ui.ts # UI configuration├── services/ # API and business logic services│ └── apiService.ts # API service layer├── components/ # Reusable UI components│ ├── generic/ # Generic components│ └── specific/ # Domain-specific components├── hooks/ # Custom React hooks│ ├── useApiData.ts # API data fetching hook│ └── useAuth.ts # Authentication hook└── pages/ # Page components
Step 9: Replace Duplicated Code with Imports
Update files to use shared modules instead of duplicated code:
// Before - duplicated logicfunction calculateTotal(items: any[]): number {let total = 0for (const item of items) {total += item.price}return total}function calculateSum(items: any[]): number {let sum = 0for (const item of items) {sum += item.amount}return sum}function calculateAverage(items: any[]): number {let sum = 0for (const item of items) {sum += item.value}return sum / items.length}// After - extracted to shared utilityimport { calculateArraySum } from '../utils/arrayUtils'function calculateTotal(items: any[]): number {return calculateArraySum(items, 'price')}function calculateSum(items: any[]): number {return calculateArraySum(items, 'amount')}function calculateAverage(items: any[]): number {return calculateArraySum(items, 'value') / items.length}
Shared utility function:
// In utils/arrayUtils.tsexport function calculateArraySum<T>(items: T[], key: keyof T): number {return items.reduce((sum, item) => sum + (item[key] as number), 0)}export function calculateArrayAverage<T>(items: T[], key: keyof T): number {if (items.length === 0) return 0const sum = calculateArraySum(items, key)return sum / items.length}
Step 10: Verify Refactoring
Ensure code compiles and tests pass after refactoring:
# Check TypeScript compilationnpx tsc --noEmit# Run testsnpm run test# Build projectnpm run build# Run lintingnpm run lint
Refactoring Verification Checklist:
- [ ] No TypeScript compilation errors
- [ ] All tests pass
- [ ] No new linting errors introduced
- [ ] Removed all identified duplicate code
- [ ] Shared modules are properly exported
- [ ] Imports use correct relative/absolute paths
- [ ] Folder structure is logical and organized
Best Practices
DRY Principles:
- Single Responsibility: Each function/module should have one clear purpose
- Composition over Inheritance: Prefer composition for code reuse
- Immutability: Use immutable data structures where possible
- Type Safety: Leverage TypeScript's type system to prevent runtime errors
- Extract Early: Refactor duplication as soon as you identify it
- Utility-First: Create reusable utilities before business logic
TypeScript-Specific Best Practices:
- Interfaces Over Types: Use interfaces for object shapes, types for unions/primitives
- Utility Types: Use Pick, Omit, Partial, Record for type transformations
- Generics: Use generics for reusable components and functions
- Type Guards: Use type guards for runtime type checking
- Never Types: Avoid
any- use proper type definitions - Readonly: Mark properties as readonly where appropriate
Code Organization:
- Feature-Based Folders: Group related files in feature directories
- Shared Resources: Keep shared utilities in dedicated directories
- Index Files: Use index.ts files for clean imports
- Barrel Exports: Export related items from single index file
- Separation of Concerns: Keep UI, business logic, and data separate
Refactoring Workflow:
- Analyze First: Identify all duplication before making changes
- Small Steps: Refactor incrementally, test after each change
- Test Coverage: Ensure tests exist for refactored code
- Git Commits: Commit refactoring in logical chunks
- Backward Compatibility: Maintain existing public APIs
Common Issues
Breaking Changes After Refactoring
Issue: Refactoring breaks existing code that imports refactored modules
Solution:
- Use git bisect to identify which commit broke functionality
- Check import paths and ensure they're correct
- Verify exported interfaces match what consumers expect
- Add index.ts files with proper re-exports
- Run tests frequently during refactoring
Circular Dependencies
Issue: Extracted utilities create circular dependencies
Solution:
- Analyze dependency graph before extraction
- Split utilities into smaller, more focused modules
- Use dependency injection where appropriate
- Consider merging closely related utilities
- Move shared types to separate types/ directory
Type Errors After Consolidation
Issue: Merged types cause TypeScript compilation errors
Solution:
- Use intersection types when combining similar interfaces
- Use utility types (Pick, Omit, Partial) to transform types
- Add proper type guards for runtime type checking
- Review generic constraints and type parameters
- Use
satisfieskeyword for complex type constraints
Over-Engineering
Issue: Creating overly complex generic abstractions
Solution:
- Start with concrete implementations, extract abstractions later
- Prefer composition over complex inheritance
- Keep generics simple with clear constraints
- Use type assertions sparingly and only when necessary
- Focus on actual duplication, not theoretical abstraction
Advanced Refactoring Patterns
Higher-Order Components
Wrap components with additional behavior:
// In components/withLoading.tsxinterface WithLoadingProps {isLoading: booleanloadingText?: string}export function withLoading<P>(Component: React.ComponentType<P>,props: P & WithLoadingProps) {if (props.isLoading) {return (<div><Component {...props} disabled={true} /><div className="loading-overlay">{props.loadingText || 'Loading...'}</div></div>)}return <Component {...props} />}// Usageexport function UserList({ users, loading }: UserListProps) {return (<div><GenericListitems={users}renderItem={(user) => <span>{user.name}</span>}onSelectItem={onSelectUser}/></div>)}export function LoadingUserList({ users, loading }: UserListProps) {return (<div><GenericListitems={users}renderItem={(user) => <span>{user.name}</span>}onSelectItem={onSelectUser}/></div>)}
Factory Pattern
Create objects without specifying exact classes:
// In utils/dataFetcher.tsinterface DataFetcher<T> {fetch(id: string): Promise<T>}class UserDataFetcher implements DataFetcher<User> {async fetch(id: string): Promise<User> {const response = await fetch(`/api/users/${id}`)return response.json()}}class ProductDataFetcher implements DataFetcher<Product> {async fetch(id: string): Promise<Product> {const response = await fetch(`/api/products/${id}`)return response.json()}}// Factory functionexport function createDataFetcher<T>(type: 'user' | 'product'): DataFetcher<T> {switch (type) {case 'user':return new UserDataFetcher() as DataFetcher<T>case 'product':return new ProductDataFetcher() as DataFetcher<T>}}// Usageconst userFetcher = createDataFetcher<User>('user')const productFetcher = createDataFetcher<Product>('product')const user = await userFetcher.fetch('123')const product = await productFetcher.fetch('456')
Repository Pattern
Centralize data access logic:
// In repositories/UserRepository.tsclass UserRepository {private apiUrl: string = '/api/users'async findById(id: string): Promise<User> {const response = await fetch(`${this.apiUrl}/${id}`)return response.json()}async findAll(): Promise<User[]> {const response = await fetch(this.apiUrl)return response.json()}async create(user: Omit<User, 'id'>): Promise<User> {const response = await fetch(this.apiUrl, {method: 'POST',body: JSON.stringify(user)})return response.json()}async update(id: string, user: Partial<User>): Promise<User> {const response = await fetch(`${this.apiUrl}/${id}`, {method: 'PUT',body: JSON.stringify(user)})return response.json()}async delete(id: string): Promise<void> {await fetch(`${this.apiUrl}/${id}`, { method: 'DELETE' })}}export const userRepository = new UserRepository()// Usage in componentsimport { userRepository } from '../repositories/UserRepository'const user = await userRepository.findById('123')const allUsers = await userRepository.findAll()await userRepository.create({ name: 'John', email: 'john@example.com' })
Troubleshooting Checklist
Before refactoring:
- [ ] Codebase has been analyzed for duplication patterns
- [ ] Duplication types have been categorized
- [ ] Target files/modules for refactoring identified
- [ ] Tests exist for code to be refactored
During refactoring:
- [ ] Each step is tested before moving to next
- [ ] No TypeScript compilation errors introduced
- [ ] Existing tests still pass
- [ ] Code is committed in logical chunks
- [ ] Import paths are verified after each change
After refactoring:
- [ ] All identified duplication has been eliminated
- [ ] Code compiles without errors
- [ ] All tests pass
- [ ] No new linting errors
- [ ] Folder structure is organized and logical
- [ ] Shared modules are properly exported
- [ ] Documentation is updated if needed
Related Skills
linting-workflow: Ensure code quality during refactoringdocstring-generator: Add documentation to refactored codetypescript-dry-principle: This skillnextjs-pr-workflow: Create PR after completing refactoringtest-generator-framework: Generate tests for refactored code