Skip to main content

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

  1. Install TypeScript:
npm install --save-dev typescript @types/node
  1. 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

  1. Start with New Code

    • Write all new code in TypeScript
    • Leave existing code as JavaScript initially
  2. Enable Incremental Migration

    {
    "compilerOptions": {
    "allowJs": true,
    "checkJs": true
    }
    }
  3. Migrate File by File

    • Start with simpler files
    • Move to more complex ones
    • Update imports as you go
  4. Add Type Checking Gradually

    {
    "compilerOptions": {
    "strict": false,
    "noImplicitAny": true, // Enable first
    "strictNullChecks": true // Enable later
    }
    }

Testing During Migration

  1. 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');
});
});
  1. Add Type Coverage Tests
npm install --save-dev type-coverage

# Add to package.json
{
"scripts": {
"type-coverage": "type-coverage --detail"
}
}

Best Practices

  1. Start Small

    • Migrate one module at a time
    • Begin with self-contained utilities
  2. Use TypeScript's Configuration Options

    • Start with less strict options
    • Gradually enable stricter checks
  3. Maintain a Migration Tracker

    - [x] Utils module
    - [x] API client
    - [ ] React components
    - [ ] Tests
  4. Document Patterns

    • Create guidelines for the team
    • Document common type patterns
  5. Review and Refactor

    • Regular type safety reviews
    • Refactor as types evolve

Remember:

  1. Migration is a process, not an event
  2. Use TypeScript features gradually
  3. Keep the application working throughout
  4. Test thoroughly during migration
  5. Document decisions and patterns