Wes Bragavim-wes
Platform Engineering

Enhancing Development Efficiency: Migrating to TypeScript for JavaScript Projects

December 11, 2023
typewriter

Why Adopt TypeScript in Your Development Workflow?

Imagine working in a codebase with more than five developers, with the potential to add even more to the team. In today's remote work environment, it's common for teams to be distributed across various time zones and regions.

Pull requests flood in at all hours; peer reviews may not catch everything, and the development and QA processes can be slow, leaving room for a subset of issues to slip through. Silly bugs are bound to happen, and it's certainly not enjoyable to be called in over the weekend because someone forgot to apply a null check in an array map, leading to a crash in the UI at a crucial customer interaction.

Here we are in 2023, and JavaScript tooling has evolved to a point where these types of issues are increasingly likely to be caught right at the developer's machine, rather than surfacing in production. This means customers won't face frustrating roadblocks like being unable to proceed past the checkout due to a 'Cannot read property 'x' of undefined' error.

TypeScript: Elevating Development Standards

TypeScript is a superset of JavaScript that adds static typing capabilities to the language, providing a wide array of benefits such as enhanced code maintainability, improved developer productivity, and better tooling support. Contrary to popular belief, TypeScript is not a separate language. In fact, if you're using any modern code editor, such as VSCode or WebStorm, you are already leveraging TypeScript as a language server, which comes built-in the editor, powering autocompletion features in your JavaScript files. While not utilizing its full capabilities, you are benefiting from some of its features.

Preventing Common Errors

Well, let's take a look at some simple yet subtle examples that would crash someone's weekend.

1. The Typed Double

JavaScript:

// Runtime errors can be costly in an enterprise application
function processOrder(order) {
  if (order.status === "approved") {
    // Process the order
  } else {
    // Handle error
  }
}

processOrder({ status: "approvedd" }); // No error is thrown, but the status is misspelled

TypeScript:

// TypeScript catches common errors during development
interface Order {
  status: "approved" | "pending" | "rejected";
  // Other properties
}

function processOrder(order: Order) {
  if (order.status === "approved") {
    // Process the order
  } else {
    // Handle error
  }
}

// TypeScript error: Object literal may only specify known properties
// You would see an indication in your editor showing where the error is located
processOrder({ status: "approvedd" }); 

2. The missing E

function MyComponent({ nam }) {
  return <div>{name}</div>;
}
// TypeScript error: Property 'name' does not exist on type '{ nam: any; }'.
function MyComponent({nam}){
    return <div>{name}</div>;
}

3. The mysterious case of PaymentService.run

Someone happens to be refactoring a file and decided to adjust a method name to be less redundant and forgot to update the consuming end. Trust me, this happens to the best of us.

const PaymentService = {
  async process(priceId, user){
    const purchased = await StripeClient.pay({priceId, customer_id: user.customer_id});
    const product = await UserProduct.create({productId, _user: user.id})
  }
}

module.exports = PaymentService;

// ---------------- In another file consuming this object
const PaymentService = require('./payment.service');
function onOrderEvent(message){
  if(message.type === 'CREATED'){
    // Error at runtime breaking the event consumer logic
    // And we can't collect the money for the purchase and the client doesn't get to enjoy their product 
    await PaymentService.processPayment(message.body.priceId, message.body.user);
  }
}

const PaymentService = {
  async process(priceId, user){
    const purchased = await StripeClient.pay({priceId, customer_id: user.customer_id});
    const product = await UserProduct.create({productId, _user: user.id})
  }
}

module.exports = PaymentService;

// ---------------- In another file consuming this object
const PaymentService = require('./payment.service');
function onOrderEvent(message){
  if(message.type === 'CREATED'){
    // TypeScript: Property ‘processPayment’ does not exist on type PaymentService
    await PaymentService.processPayment(message.body.priceId, message.body.user);
  }
}

What about checking an entire codebase for errors?

Simple, just npx tsc --noEmit. That will pick up the tsconfig.json in the project and adjust to the rules defined. Giving a nice output that would fail CI in the case that you had this running as part of your pull-request checks.

npx tsc --noEmit
src/components/App.tsx:2971:44 - error TS2339: Property 'k' does not exist on type 'KeyboardEvent | React.KeyboardEvent<Element>'.
  Property 'k' does not exist on type 'KeyboardEvent'.

2971         const shape = findShapeByKey(event.k);
                                                ~


Found 1 error in src/components/App.tsx:2971

Large codebases

We don't have to migrate every single file at once in a large codebase. We can leverage a seamless migration where TypeScript is set up to process JavaScript files too, and that way, we can introduce and convert current files as work is being done. Set a goal to write any new JavaScript files in TypeScript and migrate one extra file at a time or more to be fully typed. Track those files migrated and make it known for everyone what percentage of the code has been converted.

A Strategic Migration Plan

Migrating from JavaScript to TypeScript in a large codebase requires a strategic approach to ensure continual product development benefits an organization. A pragmatic migration plan, particularly in a React codebase, involves:

1. Ensuring React Build Stability

Prioritize a stable React build during migration, enabling seamless operation in both JavaScript and TypeScript environments.

2. Collaborative File Migration Approach

Incorporate a strategy where each pull request includes a set number of files migrating from JavaScript to TypeScript, ensuring developers contribute to this evolution. Utilize tools like cloc for tracking TypeScript vs. JavaScript file counts.

npx cloc --include-lang=JavaScript,TypeScript src --by-percent c

3. Focused Migration - API Related Files and Interfaces

Start the migration with API-related files, creating interfaces to document their usage, providing significant initial benefits to consumers.

import axios, { AxiosResponse } from 'axios';

interface User {
  id: number;
  name: string;
  email: string;
}

class ApiService {
  async getUsers(): Promise<User[]> {
    try {
      const response: AxiosResponse = await axios.get('/api/users');
      return response.data;
    } catch (error) {
      throw new Error('Failed to fetch users');
    }
  }

  async createUser(user: User): Promise<User> {
    try {
      const response: AxiosResponse = await axios.post('/api/users', user);
      return response.data;
    } catch (error) {
      throw new Error('Failed to create user');
    }
  }

  async updateUser(user: User): Promise<User> {
    try {
      const response: AxiosResponse = await axios.put(`/api/users/${user.id}`, user);
      return response.data;
    } catch (error) {
      throw new Error('Failed to update user');
    }
  }

  async deleteUser(userId: number): Promise<void> {
    try {
      await axios.delete(`/api/users/${userId}`);
    } catch (error) {
      throw new Error('Failed to delete user');
    }
  }
}

export default new ApiService();

4. Migrating Shared Library and Utility Functions

Focus on migrating shared logic, residing in folders like lib/, shared/, or utils/, ensuring proper typing and seamless integration.

5. Transitioning Components and Hooks

As the more involved aspect of migration, prioritize TypeScript for developing new components and gradually convert related files.

Embracing TypeScript for a Seamless Development Future

In the ever-evolving coding universe, TypeScript stands tall as a vital asset. Its knack for pre-empting bugs elevates our development experience, shielding us from weekend-haunting errors and frustrating glitches.

From catching mistyped properties to fortifying code reviews, TypeScript redefines our approach to software craftsmanship. Its incremental adoption promises a smoother transition toward robust, maintainable codebases.

In this landscape, TypeScript isn't just an option; it's a necessity. It leads us toward a future where today's bug prevention translates into seamless, delightful user experiences tomorrow.

Let's embrace TypeScript as more than a language tool—a trusted partner in crafting exceptional software, lighting the path to a bug-free, brighter future.