Migrating from JavaScript to TypeScript
This guide will help you migrate your JavaScript projects to TypeScript, following best practices and a step-by-step approach.
Preparation
- Install TypeScript:
npm install --save-dev typescript @types/node
- Create tsconfig.json:
{
"compilerOptions": {
"target": "es2020",
"module": "commonjs",
"strict": false, // Start with false, enable later
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./dist",
"rootDir": "./src",
"allowJs": true, // Allow JavaScript files
"checkJs": true // Type check JavaScript files
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
Step-by-Step Migration
1. Rename Files
Start by renaming your .js files to .ts or .tsx (for React components):
# Example structure
src/
├── index.js → index.ts
├── utils/
│ ├── helpers.js → helpers.ts
│ └── config.js → config.ts
└── components/
├── App.jsx → App.tsx
└── Button.jsx → Button.tsx
2. Fix Import/Export Syntax
Update your imports and exports to use ES6 module syntax:
// Before
const express = require('express');
module.exports = { someFunction };
// After
import express from 'express';
export { someFunction };
3. Add Type Definitions
Start adding types gradually:
// Before (JavaScript)
function calculateTotal(items) {
return items.reduce((total, item) => total + item.price, 0);
}
// After (TypeScript)
interface Item {
id: string;
price: number;
quantity: number;
}
function calculateTotal(items: Item[]): number {
return items.reduce((total, item) => total + item.price, 0);
}
4. Handle Third-Party Libraries
Install type definitions for your dependencies:
# Example
npm install --save-dev @types/react @types/express @types/lodash
Create custom declarations when needed:
// custom.d.ts
declare module 'untyped-module' {
export function someFunction(): void;
export const someValue: string;
}
5. Update React Components
Convert React components to TypeScript:
// Before (JavaScript)
const Button = ({ onClick, children }) => (
<button onClick={onClick}>{children}</button>
);
// After (TypeScript)
interface ButtonProps {
onClick: () => void;
children: React.ReactNode;
}
const Button: React.FC<ButtonProps> = ({ onClick, children }) => (
<button onClick={onClick}>{children}</button>
);
6. Handle API Responses
Add types for API responses:
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
interface User {
id: string;
name: string;
email: string;
}
async function fetchUser(id: string): Promise<ApiResponse<User>> {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
Common Migration Challenges
1. Any Type
Use any temporarily when migrating, but plan to replace it:
// Temporary
let data: any = fetchData();
// Better
interface ApiData {
id: string;
value: number;
}
let data: ApiData = fetchData();
2. Mixed Types
Handle cases where a value could have multiple types:
// Before
function process(value) {
if (typeof value === 'string') {
return value.toUpperCase();
}
return String(value);
}
// After
function process(value: string | number): string {
if (typeof value === 'string') {
return value.toUpperCase();
}
return String(value);
}
3. Class Properties
Add types to class properties and methods:
// Before
class User {
constructor(data) {
this.name = data.name;
this.age = data.age;
}
getInfo() {
return `${this.name} is ${this.age}`;
}
}
// After
interface UserData {
name: string;
age: number;
}
class User {
private name: string;
private age: number;
constructor(data: UserData) {
this.name = data.name;
this.age = data.age;
}
getInfo(): string {
return `${this.name} is ${this.age}`;
}
}
Gradual Migration Strategy
-
Start with New Code
- Write all new code in TypeScript
- Leave existing code as JavaScript initially
-
Enable Incremental Migration
{
"compilerOptions": {
"allowJs": true,
"checkJs": true
}
} -
Migrate File by File
- Start with simpler files
- Move to more complex ones
- Update imports as you go
-
Add Type Checking Gradually
{
"compilerOptions": {
"strict": false,
"noImplicitAny": true, // Enable first
"strictNullChecks": true // Enable later
}
}
Testing During Migration
- Update Test Files
// Before
const { expect } = require('chai');
// After
import { expect } from 'chai';
import { User } from '../src/models/User';
describe('User', () => {
it('should create user', () => {
const user = new User({ name: 'Test', age: 30 });
expect(user.getInfo()).to.equal('Test is 30');
});
});
- Add Type Coverage Tests
npm install --save-dev type-coverage
# Add to package.json
{
"scripts": {
"type-coverage": "type-coverage --detail"
}
}
Best Practices
-
Start Small
- Migrate one module at a time
- Begin with self-contained utilities
-
Use TypeScript's Configuration Options
- Start with less strict options
- Gradually enable stricter checks
-
Maintain a Migration Tracker
- [x] Utils module
- [x] API client
- [ ] React components
- [ ] Tests -
Document Patterns
- Create guidelines for the team
- Document common type patterns
-
Review and Refactor
- Regular type safety reviews
- Refactor as types evolve
Remember:
- Migration is a process, not an event
- Use TypeScript features gradually
- Keep the application working throughout
- Test thoroughly during migration
- Document decisions and patterns