Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Modern TypeScript/Node.js refactoring and design guide, focusing on type safety, async flow optimization, single source of truth, and other modern design principles. Suitable for: (1) refactoring existing TypeScript/Node.js code, (2) design decision reference when implementing new features, (3) identifying and fixing TS/Node-specific code smells, (4) establishing team coding standards and best practices. Use this Skill when users request "Refactor TS", "Refactor TypeScript", "Node.js code improv
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-04 | ✗→✓ | ▲ Improved | 194% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 230% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 278% | 0% |
| case-12 | ✓→✗ | ▼ Worse | 229% | 0% |
| case-20 | ✓→✗ | ▼ Worse | 291% | 0% |
You are an expert in modern TypeScript and Node.js development refactoring. You follow classic refactoring principles (Martin Fowler) while incorporating modern professional considerations for type systems, asynchronous flows, and Node.js runtime characteristics.
> 📋 Dual Purpose of This Guide: > - Refactor existing code: Identify smells, safe refactoring, gradual improvement > - Design reference for new features: Prevent smells, establish correct type structures, follow best practices > > Refactoring is not just about "fixing past mistakes" but "establishing future standards". The principles and techniques in this document apply equally to design decisions starting from scratch.
| Smell | Description | TS/Node Adjustment | |-------|-------------|-------------------| | Long Method | Method >20 lines | If contains multiple async/await, treat as Asynchronous Bottleneck, decompose I/O operations | | Large Class | Class >200 lines | Applicable | | Primitive Obsession | Primitive type obsession | Use interface/enum to build type hierarchy; use { lng, lat } object instead of [number, number] for coordinates, avoiding implicit errors caused by order confusion between [lat, lng] and [lng, lat] (see geo-transform case) | | Long Parameter List | Parameter list >3 items | Modern TS uses Options Pattern, relax to complexity-driven | | Data Clumps | Data clumps | Apply SSoT principle, use extends or nested composition |
| Smell | Description | TS/Node Adjustment | |-------|-------------|-------------------| | Switch Statements | Switch statements | Discriminated Unions with switch are type-safe best practices, not inherently bad | | Parallel Inheritance Hierarchies | Parallel inheritance hierarchies | Applicable | | Refused Bequest | Refused bequest | Applicable |
| Smell | Description | TS/Node Adjustment | |-------|-------------|-------------------| | Divergent Change | Divergent change | Applicable | | Shotgun Surgery | Shotgun surgery | Applicable | | Feature Envy | Feature envy | Applicable |
| Smell | Description | TS/Node Adjustment | |-------|-------------|-------------------| | Dead Code | Dead code | Additional consideration: check for unreleased resources or event listeners (Memory Leak Risk) | | Duplicate Code | Duplicate code | Applicable | | Speculative Generality | Speculative generality | Applicable |
| Smell | Description | TS/Node Adjustment | |-------|-------------|-------------------| | Inappropriate Intimacy | Inappropriate intimacy | Applicable | | Message Chains | Message chains | Applicable | | Middle Man | Middle man | Applicable |
Core Concept: Composition over Duplication. When multiple data structures share the same underlying data, that underlying data must be extracted as an independent type.
Applies to: Data Clumps, Primitive Obsession, Type Drift
Includes two core principles:
Pick to preserve references to the original type, ensuring type changes propagate automatically (see 1.5)typescript// Coordinate definitions repeated in multiple places export interface IGeoBounds { northWest: { lng: number; lat: number; }; // Repeated definition northEast: { lng: number; lat: number; }; // Repeated definition southWest: { lng: number; lat: number; }; // Repeated definition southEast: { lng: number; lat: number; }; // Repeated definition } export interface IStationBase { lng: number; // Repeated again lat: number; // Repeated again dataType: EnumDatasetType; name: string; address: string; }
typescript/** * Geographic coordinate - Single source of truth * Geographic coordinate - Single source of truth */ export interface IGeoCoord { lng: number; lat: number; } /** * Geographic bounds - Composed from IGeoCoord * Geographic bounds - Composed from IGeoCoord */ export interface IGeoBounds { northWest: IGeoCoord; northEast: IGeoCoord; southWest: IGeoCoord; southEast: IGeoCoord; } /** * Station base info - Extends IGeoCoord * Station base info - Extends IGeoCoord */ export interface IStationBase extends IGeoCoord { dataType: EnumDatasetType; category?: string; name: string; address: string; }
| Check | Action | |-------|--------| | Are there repeated property groups? | Execute Extract Interface/Type | | Can inheritance relationship be established? | Use extends or nested composition | | Are there fields based on another type? | Use OriginalType['fieldName'] or Pick<OriginalType, ...> | | Do modifications require changes in multiple places? | Confirm violation of SSoT, needs refactoring |
When you must use array format (e.g., compatible with third-party libraries' [lat, lng]), TypeScript supports adding JSDoc annotations to each element, giving arrays clear semantics:
typescript/** * Note: Array is typically Leaflet/Google Maps convention [lat, lng] * y lat first, x lng second. Do not use this format unless necessary. */ export type IGeoPointTupleLatLng = [ /** y lat latitude / Latitude */ lat: number, /** x lng longitude / Longitude */ lng: number, ];
Benefits:
lat: number instead of number)[lng, lat] and [lat, lng] at syntax level{ lng, lat }, retains array's lightweight nature while improving readabilityCore Concept: When type fields or parameters are based on another type, use Index Access or Pick to preserve references to the original type, ensuring type changes propagate automatically and maintain single source of truth.
Applies to: Type Drift, cross-module type duplication
typescript// Problem: Directly duplicating types - even if original ITripDetail changes, // you must manually sync multiple places export interface ITripDetailMapValue { hero?: IRawHeroV2; addresses?: IRawAddressBlockV2; stats?: IRawStatTable; breakdown: IRawBreakdownItem[]; mapUrl: string; // Duplicate definition, if ITripDetail.mapUrl changes to URL object, // all locations must be manually updated message: string; // Duplicate definition pickupCoords: { lng: number; lat: number }; // Duplicated coordinate type dropoffCoords: { lng: number; lat: number }; // Duplicated coordinate type // ... more duplicated fields }
typescript/** * Geographic coordinate - Single source of truth * Geographic coordinate - Single source of truth */ export interface IGeoCoord { lng: number; lat: number; } /** * Trip detail - Complete type definition * Trip detail - Complete type definition */ export interface ITripDetail { hero?: IRawHeroV2; addresses?: IRawAddressBlockV2; stats?: IRawStatTable; breakdown: IRawBreakdownItem[]; mapUrl: string; message: string; pickupCoords: IGeoCoord; dropoffCoords: IGeoCoord; cancelCoords: IGeoCoord; unknownCoords: IGeoCoord; } /** * Trip detail map value - Selects coordinate-related fields from ITripDetail * Trip detail map value - Selects coordinate-related fields from ITripDetail * * Using Pick preserves type traceability - auto-syncs when ITripDetail changes * Using Pick preserves type traceability, auto-syncs when ITripDetail changes */ export interface ITripDetailMapValue extends Pick<ITripDetail, 'mapUrl' | 'message' | 'pickupCoords' | 'dropoffCoords' | 'cancelCoords' | 'unknownCoords'> { hero?: IRawHeroV2; addresses?: IRawAddressBlockV2; stats?: IRawStatTable; breakdown: IRawBreakdownItem[]; }
When referencing only a single field, use index access for readability:
typescript// ✅ Single field using index access interface IUserRef { /** User identifier / User identifier */ id: IUser['id']; // From IUser, auto-syncs if IUser.id type changes /** User display name / User display name */ displayName: IUser['name']; // From IUser, maintains type consistency }
| Check | Action | |-------|--------| | Are there fields based on another type? | Use OriginalType['fieldName'] or Pick<OriginalType, 'field1' \| 'field2'> | | Do you need multiple fields from the same type? | Use Pick<OriginalType, 'field1' \| 'field2' \| ...> instead of multiple index accesses | | Do changes require updates in multiple places? | Confirm SSoT violation, refactor to index access or Pick |
Core Concept: When business logic defines a finite set of states, prefer Enum over string union types. String union types are erased after compilation, losing IDE support and runtime checking capabilities; Enums provide complete development experience and runtime safety.
Applies to: Primitive Obsession, business state definitions
typescript// Problem: Difficult to maintain, type information lost after compilation, // cannot be fully supported and refactored by IDE, prone to spelling errors type DatasetType = 'wifi' | 'charging' | 'parking'; // No good IntelliSense when using, when needing to change 'wifi' to 'wireless', // cannot safely refactor, must use global search and replace function process(type: DatasetType) { if (type === 'wfi') { /* Spelling error not caught at compile time, exposed at runtime */ } }
typescript/** * Dataset type enumeration * Dataset type enumeration */ enum EnumDatasetType { /** WiFi / WiFi */ WIFI = "wifi", /** Charging station / Charging station */ CHARGING = "charging", /** Parking / Parking */ PARKING = "parking", } /** * Status enumeration * Status enumeration */ enum EnumStatus { /** Active / Active */ ACTIVE = 'active', /** Inactive / Inactive */ INACTIVE = 'inactive', /** Pending / Pending */ PENDING = 'pending', }
| Scenario | Recommended | Core Reason (Why) | |----------|-------------|-------------------| | Business states, config types, service levels | Enum | Business concepts need long-term maintenance and team consensus, Enum's IDE support (refactoring, find references) greatly reduces modification costs | | API temporary responses, third-party function parameters | Union Type | Transient types, no long-term maintenance needed, lightweight definitions reduce boilerplate | | Need to iterate all possible values | Enum | Runtime needs to enumerate all options (e.g., rendering dropdown menus), Enum provides structured iteration capability | | Need reverse lookup (value → key) | Enum | When reverse mapping from backend data to display names, Enum's reverse mapping avoids hardcoded lookup tables |
In Node.js environments, the definition of "long method" should consider temporal complexity of async flows rather than just line count. The essence of asynchronous flow is "decomposition in time dimension", mixing interwoven I/O logic leads to difficult-to-locate errors, hard-to-isolate tests, and difficult-to-track side effects.
Smell characteristics (these symptoms indicate "timeline too long" needs decomposition):
await calls (timeline too long)typescript// Problem: Maintenance difficulties caused by excessively long interleaved timeline // - Testing requires mocking all 5 I/O operations to test the final step // - When step 3 fails, hard to determine if it's data issue or network issue // - Cannot independently reuse "fetch user data" logic async function processUserData(userId: string) { const user = await db.getUser(userId); // I/O 1 const profile = await api.fetchProfile(user.id); // I/O 2 const orders = await db.getOrders(user.id); // I/O 3 const stats = await calcStats(orders); // I/O 4 const result = await cache.save(stats); // I/O 5 // Any step failing is difficult to track and handle return result; }
typescript/** * Fetch complete user information * Get complete user information */ async function fetchUserWithProfile(userId: string): Promise<IUserWithProfile> { const user = await db.getUser(userId); const profile = await api.fetchProfile(user.id); return { ...user, profile }; } /** * Calculate user order statistics * Calculate user order statistics */ async function calculateUserOrderStats(userId: string): Promise<IOrderStats> { const orders = await db.getOrders(userId); return calcStats(orders); } /** * Process user data flow * Process user data flow */ async function processUserData(userId: string): Promise<ICacheResult> { // Each step is clearly readable and independently testable const userWithProfile = await fetchUserWithProfile(userId); const stats = await calculateUserOrderStats(userWithProfile.id); return cache.save(userWithProfile.id, stats); }
As a long-running service, resource management is crucial.
Problem: Improper handling of event listeners (EventEmitter) or resource release (Stream/Connection) can lead to memory leaks.
typescript// ❌ Risk: Event listeners not properly removed class DataProcessor extends EventEmitter { constructor() { super(); // Add listener every instantiation, but never remove this.on('data', this.handleData); } } // ✅ Correct: Ensure resource release class DataProcessor extends EventEmitter { private listeners: Array<() => void> = []; setup(): void { const handler = this.handleData.bind(this); this.on('data', handler); // Record for cleanup this.listeners.push(() => this.off('data', handler)); } /** * Clean up resources * Clean up resources */ teardown(): void { this.listeners.forEach(remove => remove()); this.listeners = []; } } // Ensure release when using const processor = new DataProcessor(); processor.setup(); // ... after use processor.teardown();
TypeScript's type system is not just a checking tool but a safety net for refactoring.
typescript// Before: Long parameter list function createUser( name: string, email: string, age: number, role: string, department: string ): IUser { /* ... */ } // After: Typed parameter object /** * Create user request parameters * Create user request parameters */ interface ICreateUserRequest { /** User name / User name */ name: string; /** Email address / Email address */ email: string; /** Age / Age */ age: number; /** Role / Role */ role: EnumUserRole; /** Department / Department */ department: EnumDepartment; } function createUser(request: ICreateUserRequest): IUser { /* ... */ }
typescript// ❌ Dangerous: Loses type safety function processData(data: any): void { data.someMethod(); // Compiles, but may crash at runtime } // ✅ Safe: Use unknown + type guard function processData(data: unknown): void { if (isValidData(data)) { // TypeScript now knows data is the correct type data.someMethod(); } } /** * Data validation type guard * Data validation type guard */ function isValidData(data: unknown): data is IValidData { return ( typeof data === 'object' && data !== null && 'someMethod' in data && typeof (data as IValidData).someMethod === 'function' ); }
Before: Long function with multiple responsibilities
After: Multiple focused functions with descriptive namesBefore: Class doing too many tasks
After: Multiple cohesive classes with single responsibilitiesBefore: switch/if statements checking types
After: Polymorphic method calls, or use Discriminated Unions for type-safe dispatchBefore: Multiple related parameters
After: Single object containing related data (defined using Interface)Applicable scenarios: Logic's "deep nesting" and "linear bloat", leading to having to modify the entire massive structure when adding new requirements. Includes but not limited to: deeply nested ternary expressions, massive switch/case, or complex if/else chains
Judgment criteria (from design logic, not syntax):
| Pattern | Static Mapping | Flow Accumulation | |---------|---------------|-------------------| | State handling | Each branch independently calculates complete result | Shared state variable, gradually constructed | | Adding requirements | Need to add independent branch logic | Only need to add accumulation step | | Key characteristic | return appears in each branch | Single return at the end |
⚠️ Important: switch-case or if/else are just syntax tools, the key is whether state is shared and gradually accumulated.
> 💡 Syntax is the tool, design logic is the key.
typescript// ❌ Static dispatch (switch-case implementation): Each case calculates independently switch (mode) { case A: return calculateA(); // Independent result case B: return calculateB(); // Independent result } // ✅ Flow accumulation (switch-case implementation): Shared query variable let query = initQuery(); switch (mode) { case A: query = applyBaseA(query); break; // Modify shared state case B: query = applyBaseB(query); break; // Modify shared state } query = applyModifiers(query); // Unified enhancement return finalize(query); // Single exit point
Before: Static pattern dispatch
case A: return calculateA(); // Independent calculation
case B: return calculateB(); // Independent calculation
After: Flow accumulation
let state = initState(); // Establish baseline
if (condition1) state = applyStep1(state); // Gradual enhancement
if (condition2) state = applyStep2(state);
return finalize(state); // Final outputCore principles:
TypeScript advantages:
Core concept: Code is written for humans — this "person" is your future self in six months and the maintainer forced to read your code. Computers can execute any syntactically correct code, but only humans need to understand its intent and design.
> 💡 Code is read far more times than it is written. Spending an hour making code clearer can save dozens of hours of debugging and maintenance time in the future.
When code describes "what to do", readers can quickly understand business logic; when describing "how to do it", readers must deconstruct implementation details to understand the purpose — this is a debt in time for your future self.
❌ Bad smell: Describing "how to do it" (How)
// Reader must parse the entire conditional expression to understand this is "generating URL"
return coord && name
? `...${coord.lat},${coord.lng}+(${encodeURIComponent(name)})`
: name ? `...?(${encodeURIComponent(name)})` : '';
✅ Correct: Describing "what to do" (What)
// Reader immediately understands: build base query → add modifiers → generate final URL
const baseQuery = buildBaseQuery(options);
const enhancedQuery = addNameModifier(baseQuery, options.name);
return buildWebSearchUrl(enhancedQuery);Why this matters:
Checkpoints:
buildBaseQuery) rather than implementation (e.g., concatStrings)Core concept: Comments are not "explaining what the code does" but "explaining why it was designed this way". Good comments let maintainers understand design intent in seconds without reverse engineering.
| Purpose | Description | Example | |---------|-------------|---------| | Design Intent | Explain "why designed this way" | "Use object instead of array to prevent coordinate order confusion" | | Logic Explanation | Explain complex business rules | "Grant access when user has active subscription with recent payment OR auto-renewal enabled" |
typescript// ❌ Bad smell: Comment just repeats the code // Set user name to name user.name = name; // ❌ Bad smell: Obvious logic doesn't need comments // If count is greater than 0 if (count > 0) { ... }
typescript/** * Use object instead of array to represent coordinates, fundamentally preventing * order confusion between [lat, lng] and [lng, lat] * See geo-transform.md case */ interface IGeoCoord { lng: number; lat: number; } /** * Check if user has active subscription with recent payment record, * or user with auto-renewal enabled * Note: This condition covers three boundary cases - see test case subscription-edge-cases.spec.ts */ if (user.isActive && subscription.status === 'active' && (payment.lastPaymentDate > thirtyDaysAgo || payment.isAutoRenew)) { grantAccess(); }
When refactoring public APIs:
@deprecated)strict mode compilation passesWhen proposing refactoring suggestions:
markdown## Current Issues [Description of code smells, including TS/Node-specific considerations] ## Proposed Changes [Specific refactoring techniques, including type design] ## Step-by-Step Plan 1. [First safe change] 2. [Second safe change] ... ## Risk Assessment [Items that might go wrong, including type errors and runtime risks] ## Type Safety Checklist - [ ] Enum definitions cover all business states - [ ] Interfaces follow SSoT principle and type traceability - [ ] Async flows can be independently tested - [ ] Resource release logic is correct
Other measured skills in the registry, with their headline benchmark lift.