| name | typescript-dry-principle |
| description | Apply DRY principle to eliminate code duplication in TypeScript projects with comprehensive refactoring patterns Use when this capability is needed. |
| metadata | {"author":"darellchua2"} |
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 . -name "*.ts" -o -name "*.tsx" -not -path "*/node_modules/*" -not -path "*/dist/*" -not -path "*/build/*"
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):
function formatUserName(firstName: string, lastName: string): string {
return `${firstName.charAt(0).toUpperCase()}${firstName.slice(1).toLowerCase()} ${lastName.charAt(0).toUpperCase()}${lastName.slice(1).toLowerCase()}`
}
function 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):
export 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)}`
}
import { formatFullName } from '../utils/stringUtils'
function formatUserName(firstName: string, lastName: string): string {
return formatFullName(firstName, lastName)
}
import { formatFullName } from '../utils/stringUtils'
function formatAuthorName(firstName: , : ): {
(firstName, lastName)
}
Example 2: API Call Duplication
Before (duplicated across multiple components):
async 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()
}
async 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):
class 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 = ()
{ apiService }
(): <> {
apiService.(id)
}
{ apiService }
(): <> {
apiService.(id)
}
Step 4: Consolidate Type Definitions
Merge duplicate type definitions into shared interfaces:
Before (duplicated types in multiple files):
interface UserData {
id: string
name: string
email: string
}
interface UserInfo {
id: string
fullName: string
emailAddress: string
}
interface UserProfile {
userId: string
displayName: string
contactEmail: string
}
After (consolidated in shared types file):
export interface User {
id: string
name: string
email: string
}
import type { User } from '../types/user'
const userData: User = { }
import type { User } from '../types/user'
const userInfo: User = { }
import type { User } from '../types/user'
const userProfile: User = { }
Advanced Type Consolidation (using utility types):
export type ApiResponse<T> = {
data: T
error: string | null
status: 'success' | 'error'
}
export type PaginatedResponse<T> = {
items: T[]
total: number
page: number
pageSize: number
}
export type Optional<T> = T | null | undefined
export type Nullable<T> = T | null
export 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):
interface 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>
)
}
interface 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}
))}
)
}
After (refactored to generic component):
interface GenericListProps<T> {
items: T[]
key: keyof T
renderItem: (item: T) => React.ReactNode
onSelectItem: (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>
)
}
import { GenericList } from './GenericList'
import type { User } from '../types/user'
export function UserList({ users, onSelectUser }: { users: User[]; onSelectUser: (user: User) => void }) {
return (
<GenericList
=
=
= => {user.name}}
onSelectItem={onSelectUser}
/>
)
}
{ }
{ }
() {
(
)
}
Example 2: Generic Data Fetching Hook
Before (duplicated fetching logic in multiple components):
export 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):
interface 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.])
{ data, loading, error, : () }
}
() {
useApiData<>()
}
Step 6: Create Constants Directory
Extract scattered configuration values into shared constants:
Before (configuration scattered across files):
const API_BASE_URL = 'https://api.example.com/v1'
const MAX_RETRIES = 3
const TIMEOUT = 5000
const API_BASE_URL = 'https://api.example.com/v1'
const MAX_RETRIES = 3
const TIMEOUT = 5000
const API_BASE_URL = 'https://api.example.com/v1'
const MAX_RETRIES = 3
const TIMEOUT = 5000
After (consolidated in constants):**
export 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 const
export const UI_CONFIG = {
ANIMATION_DURATION: 300,
DEBOUNCE_DELAY: 500,
TOAST_DURATION: 3000
} as const
import { API_CONFIG } from '../constants/api'
async function fetchData() {
const response = await fetch(`${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.USERS}`)
}
import { API_CONFIG } from '../constants/api'
() {
response = ()
}
Step 7: Create Validator Utilities
Extract duplicate validation logic into shared validators:
Example: Email Validation
Before (duplicate validation in multiple places):
function validateEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
return emailRegex.test(email)
}
function checkEmail(email: string): boolean {
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
return emailPattern.test(email)
}
function isEmailValid(email: string): boolean {
const pattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
return pattern.test(email)
}
After (consolidated in validators):**
export 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)
}
(: ): {
phoneRegex =
phoneRegex.(value)
}
}
{ }
(): {
.(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:
function calculateTotal(items: any[]): number {
let total = 0
for (const item of items) {
total += item.price
}
return total
}
function calculateSum(items: any[]): number {
let sum = 0
for (const item of items) {
sum += item.amount
}
return sum
}
function calculateAverage(items: any[]): number {
let sum = 0
for (const item of items) {
sum += item.value
}
return sum / items.length
}
import { calculateArraySum } from '../utils/arrayUtils'
function calculateTotal(items: any[]): number {
return calculateArraySum(items, )
}
(): {
(items, )
}
(): {
(items, ) / items.
}
Shared utility function:
export 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 0
const sum = calculateArraySum(items, key)
return sum / items.length
}
Step 10: Verify Refactoring
Ensure code compiles and tests pass after refactoring:
npx tsc --noEmit
npm run test
npm run build
npm run lint
Refactoring Verification Checklist:
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
satisfies keyword 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:
interface WithLoadingProps {
isLoading: boolean
loadingText?: 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} />
}
export function UserList({ users, loading }: UserListProps) {
return (
<div>
<GenericList
items={users}
renderItem={(user) => <span>{user.name}}
onSelectItem={onSelectUser}
/>
)
}
() {
(
)
}
Factory Pattern
Create objects without specifying exact classes:
interface 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()
}
}
export function createDataFetcher<T>(type: 'user' | 'product'): DataFetcher<T> {
switch (type) {
:
() <T>
:
() <T>
}
}
userFetcher = createDataFetcher<>()
productFetcher = createDataFetcher<>()
user = userFetcher.()
product = productFetcher.()
Repository Pattern
Centralize data access logic:
class 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.()
}
(: , : <>): <> {
response = (, {
: ,
: .(user)
})
response.()
}
(: ): <> {
(, { : })
}
}
userRepository = ()
{ userRepository }
user = userRepository.()
allUsers = userRepository.()
userRepository.({ : , : })
Troubleshooting Checklist
Before refactoring:
During refactoring:
After refactoring:
Converted and distributed by TomeVault — claim your Tome and manage your conversions.