I tried a JavaScript to TypeScript converter. Here’s what actually happened

I’m Kayla. I run a mid-size Node + React app at work. Think 30k lines, a little messy, and full of helpers I wrote at 2 a.m. on cold coffee. I wanted TypeScript, but I didn’t want to rewrite everything by hand. So I used a JavaScript-to-TypeScript converter.

For readers who want the blow-by-blow migration diary, you can read my extended case study.

The main tool I used was ts-migrate (from Airbnb). I also used TypeStat later to fix types. And yes, I’ll show real code that changed. Some of it was great. Some parts… not so great. That’s the truth.

The setup: fast, a bit loud, but it works

I ran this on macOS using Node 18. I kept my normal build (Vite + Babel) and added TS step by step.

Here’s the exact stuff I ran on my repo:

# install once
npm i -D typescript ts-node @types/node

# init TypeScript
npx tsc --init

# quick tsconfig that worked for me
# (I turned on strict later after the first pass)

And I tweaked my tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "jsx": "react-jsx",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "strict": false,
    "noImplicitAny": false,
    "resolveJsonModule": true,
    "moduleResolution": "node",
    "baseUrl": "."
  },
  "include": ["src"]
}

Then I brought in ts-migrate:

npx ts-migrate init src
npx ts-migrate rename src
npx ts-migrate migrate src
  • init made a tsconfig if I didn’t have one.
  • rename changed .js to .ts and .jsx to .tsx.
  • migrate added types (often any) and comments to hush the compiler.

Honestly, the first run felt like a leaf blower in my code. Loud, but the yard looked cleaner.

Real example 1: a tiny helper got “any” everywhere

Before:

// src/utils/math.js
function add(a, b) {
  return a + b;
}
module.exports = { add };

After ts-migrate:

// src/utils/math.ts
export function add(a: any, b: any): any {
  return a + b;
}

It switched me to ESM export and stamped any on it. That’s normal. It’s not magic. It’s a start.

What I did next:

export function add(a: number, b: number): number {
  return a + b;
}

This took 10 seconds. But that’s the point. The tool gets you to green. You tighten types where it matters.

Real example 2: React props went from “shrug” to crisp

Before:

// src/components/Button.jsx
export default function Button({ label, onClick }) {
  return <button onClick={onClick}>{label}</button>;
}

After ts-migrate:

// src/components/Button.tsx
type Props = {
  label: any;
  onClick?: any;
};
export default function Button({ label, onClick }: Props) {
  return <button onClick={onClick}>{label}</button>;
}

I fixed it like this:

type Props = {
  label: string;
  onClick?: () => void;
};
export default function Button({ label, onClick }: Props) {
  return <button onClick={onClick}>{label}</button>;
}

React props are also a spot where I love passing in object literals; in another experiment I tried giving constructors proper named arguments and it cleaned things up even more.

I know, it’s simple. But this change made my IDE feel smart at last. Hover. Autocomplete. Safer refactors. Fewer “oops” moments.

Real example 3: a fetch wrapper got strict in a good way

Before:

// src/api/getUser.js
export async function getUser(id) {
  const res = await fetch(`/api/users/${id}`);
  if (!res.ok) throw new Error('Bad response');
  return res.json();
}

After ts-migrate:

// src/api/getUser.ts
export async function getUser(id: any): Promise<any> {
  const res = await fetch(`/api/users/${id}`);
  if (!res.ok) throw new Error('Bad response');
  return res.json();
}

Then I made a type:

type User = {
  id: string;
  name: string;
  email?: string;
};

export async function getUser(id: string): Promise<User> {
  const res = await fetch(`/api/users/${id}`);
  if (!res.ok) throw new Error('Bad response');
  const data = await res.json();
  return data as User;
}

On the backend side, I recently applied the same strictness when importing CSV to DynamoDB with a Lambda function in TypeScript—another mini-project worth a peek.

Later, I dropped the as with better parsing. But day one? This was enough. Shipped and safe.

Where TypeStat helped me clean up

After ts-migrate, I had a lot of any. So I ran TypeStat to tighten types based on how code gets used.

npm i -D typestat
npx typestat --help

My simple config (typestat.json):

{
  "projects": ["tsconfig.json"],
  "transforms": ["narrowTypes", "noImplicitAny"]
}

Then:

npx typestat

A real change it made for me:

Before:

let names: any[] = [];
names.push('Ada');

After:

let names: string[] = [];

While tightening up arrays like names, I also revisited my stance on forEach versus classic loops; my hands-on take on TypeScript’s forEach loop dives into the nuances.

Small win. But when you have 100 of these, it feels big.

What I liked

  • It was fast. My first pass on 400 files took under 10 minutes.
  • It kept the app running. Tests still passed.
  • VS Code got way smarter. Hints everywhere.
  • The path to strict was clear. I could flip flags later.

What bugged me

  • Lots of any. Expected, but still a bit meh.
  • A few // @ts-ignore comments showed up. I removed most later.
  • Mixed module stuff (CJS vs ESM) needed care. I had one import loop.
  • Jest needed tweaks. I used ts-jest and had to fix paths.

Here’s the jest bit I used:

// jest.config.js
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1'
  }
};

If you ever trip over mysterious --file or path arguments in your own scripts, my hands-on take on taming those flags might save you an hour.

Not hard. Just a little fiddly.

Want a lighter path? JSDoc works too

If you’re not ready to rename files, you can stay in .js and turn on checkJs. Add JSDoc types and tsc will help you.

tsconfig:

{
  "compilerOptions": {
    "checkJs": true
  },
  "include": ["src"]
}

Then in code:

/**
 * @param {number} a
 * @param {number} b
 * @returns {number}
 */
function add(a, b) {
  return a + b;
}

I even had to rename a few long-standing fields, and keeping the original JSDoc while doing so was trickier than I thought—here’s my hands-on review of how to pull that off.

This is great for teams that want safety first, and .ts later.

As I was working through the migration, the succinct checklist on ImprovingCode helped me prioritize which files to tackle next.

Little tips I wish someone told me

  • Convert folder by folder. Don’t swing at the whole repo on day one.
  • Keep strict off at first. Then turn it on, bit by bit.
  • Add ESLint with typescript-eslint. It catches weird stuff early.
  • Kill dead code first. Less junk means fewer any types.
  • Write a