Refactoring Legacy Code in TypeScript: From A to Z
Working with legacy code is an inevitable reality for many developers. Often, such projects are written in plain JavaScript without types, contain tangled dependencies, and lack tests. TypeScript offers a powerful toolkit for gradually improving such code, making it safer, more readable, and more maintainable. In this article, we will go from analyzing old code to creating a clean, typed architecture.
1. Preparation and Strategy: Where to Start?
Before changing code, you need to create a "safety net". The main rule of refactoring: don't break what works. Start by setting up the environment.
1.1. Initializing TypeScript and Strict Mode
Install TypeScript and create a configuration file tsconfig.json. Initially, you can disable strict checks to avoid being overwhelmed by errors, but gradually you need to enable them.
{ "compilerOptions": { "target": "ES2020", "module": "commonjs", "strict": true, // Start with false, then gradually enable "noImplicitAny": true, "strictNullChecks": true, "outDir": "./dist", "rootDir": "./src", "allowJs": true, // Allow mixing JS and TS "checkJs": false // Don't check JS files for now }, "include": ["src/**/*"]}1.2. Automation and Tests
Add a linter (ESLint) and a formatter (Prettier). If there are no tests, at least write integration tests for critical sections. This is your insurance against regressions.
- ESLint with the TypeScript plugin will catch logical errors.
- Jest or Vitest for unit testing.
- Set up pre-commit hooks (Husky) so code is checked before committing.
2. Step-by-Step Migration: From JS to TS
Don't try to rewrite everything at once. Use an iterative approach. The best tool here is allowJs: true in tsconfig. This allows you to include JavaScript files from TypeScript.
2.1. Renaming Files and Basic Typing
Start by renaming the simplest files like utils.js or helpers.js to .ts. The compiler will start complaining about implicit any. Your task is to describe function signatures.
Before (JavaScript):
function calculateDiscount(price, percent) { return price - (price * percent / 100);}After (TypeScript):
function calculateDiscount(price: number, percent: number): number { return price - (price * percent / 100);}2.2. Working with "any" and "unknown"
There is a lot of implicit any in legacy code. Don't try to type everything perfectly right away. Use any as a temporary solution, but mark it with a TODO. For data from external sources (API, localStorage), it's better to use unknown with subsequent type checking.
// Temporary solutionfunction processData(data: any) { // TODO: Add proper type console.log(data.name);}
// Correct approach for external datafunction processUserData(data: unknown) { if (typeof data === 'object' && data !== null && 'id' in data) { const user = data as { id: number; name: string }; console.log(user.name); }}3. Refactoring Complex Constructs
Once basic typing is done, move on to improving the architecture. Legacy code often suffers from "god objects" and tangled call chains.
3.1. Extracting Interfaces and Types
Instead of passing objects with an unknown structure, create interfaces. This makes the code self-documenting.
// Badfunction saveUser(user: any) { // It's unclear what fields user has}
// Go