Renaming .js to .ts — The First File

8 min read

The setup is done: TypeScript is installed, tsconfig.json has allowJs: true, and npm run type-check exits clean on an all-JavaScript project. Now the real work begins — renaming your first file. The right choice of first file determines whether this goes smoothly or turns into a week-long slog. This lesson covers how to choose it, what to expect when you rename it, and how to interpret the errors TypeScript immediately shows you.

Choosing the right first file

The worst first-file choices are the ones that seem natural: the most complex page object, the core helper that everything imports from, the legacy auth module. Those files have many callers, many implicit assumptions, and many implicit types. Fixing them means fixing a chain of errors across the whole project.

A good first file has four properties:

  • Leaf node: nothing else in your project imports from it, or very few files do. Changes to it don't cascade.
  • Small: under 100 lines. You can read the whole file in two minutes and understand every line.
  • Well-understood: no legacy workarounds, no "nobody knows why this works" magic.
  • No exotic patterns: avoid files with complex prototype manipulation, arguments objects, dynamic require() calls, or decorator syntax.

The best candidates: a utility function file (formatDate.js, buildUrl.js), a simple data factory (createTestUser.js), or a configuration file (config.js).

The renaming process

Use git mv instead of a filesystem rename — it preserves git history so git log --follow still works on the TypeScript file:

git mv src/utils/format-date.js src/utils/format-date.ts

Then run type-check immediately:

npm run type-check

TypeScript will show you errors specific to this file. Fix them all before touching another file. Commit the result as a standalone commit so the migration is easy to review and easy to revert if needed:

git add src/utils/format-date.ts
git commit -m "Migrate format-date.js to TypeScript"

Before and after: a utility file

Here is a real migration — a date formatter used in Playwright test reports.

// format-date.js (before)
function formatDate(date, format) {
  if (format === 'iso') return date.toISOString();
  if (format === 'short') return date.toLocaleDateString('en-GB');
  return date.toLocaleDateString();
}
 
function daysAgo(n) {
  const d = new Date();
  d.setDate(d.getDate() - n);
  return d;
}
 
module.exports = { formatDate, daysAgo };

After renaming to .ts, TypeScript immediately flags:

  1. Parameter 'date' implicitly has an 'any' type
  2. Parameter 'format' implicitly has an 'any' type
  3. Parameter 'n' implicitly has an 'any' type

Fix each by adding types, and convert module.exports to ES module exports:

// format-date.ts (after)
export function formatDate(date: Date, format: 'iso' | 'short' | 'long'): string {
  if (format === 'iso') return date.toISOString();
  if (format === 'short') return date.toLocaleDateString('en-GB');
  return date.toLocaleDateString();
}
 
export function daysAgo(n: number): Date {
  const d = new Date();
  d.setDate(d.getDate() - n);
  return d;
}

The migration added:

  • Parameter types that prevent calling formatDate(42, 'iso') or formatDate(date, 'yesterday')
  • A union type 'iso' | 'short' | 'long' that makes the valid formats discoverable
  • A return type : string that documents the output and catches bugs where a code path forgets to return

The same utility file — before and after migration

format-date.js

  • Parameters accept any value

  • format: any value works — 'yesterday' compiles fine

  • Caller doesn't know what to pass

  • module.exports — no IDE import resolution

  • Return type unknown to callers

format-date.ts

  • date: Date — wrong types flagged immediately

  • format: 'iso' | 'short' | 'long' — autocomplete shows options

  • Caller sees parameter names and types on hover

  • export — IDE resolves imports automatically

  • Returns string — callers know what they get

The four errors you'll see most on the first rename

Implicit any parameters. The most common error. Fix by adding a type annotation after the parameter name.

CommonJS exports. module.exports = { fn } and const x = require('./mod') don't work well in TypeScript. Convert to export function fn() and import { fn } from './mod'.

// Before (CommonJS)
const { getUser } = require('./user-helpers');
module.exports = { login };
 
// After (ES modules)
import { getUser } from './user-helpers';
export function login() { ... }

Missing module types. If the file imports from a .js file that has no types yet, TypeScript may complain about the import. With allowJs: true, this is usually fine — the imported file is typed as any. If you see a genuine error, check the Lesson 4 of this chapter on untyped modules.

Strict null checks on return values. If strictNullChecks is already enabled, Array.find() returns T | undefined — not just T. Functions that were returning T now need to handle the undefined case.

What not to do on the first file

Don't try to type everything perfectly. Add types to function parameters and return values. Leave internal variables for TypeScript to infer. If a complex object type is unclear, start with Record<string, unknown> and refine it later.

Don't enable additional strict flags. The first rename is not the moment to also turn on noImplicitAny globally. Make the file compile cleanly under the current settings, then think about tightening.

Don't rename callers at the same time. The callers are still .js files. With allowJs: true, they continue to import from the new .ts file without any changes. Migrate them separately.

⚠️ Common mistakes

  • Using git rename from the file explorer instead of git mv. Most file explorers perform a copy-and-delete rather than a rename — git treats this as a deleted file and a new file, breaking git log --follow. Always use git mv.
  • Fixing errors by adding : any everywhere. If your first converted file has twenty : any annotations, you've added TypeScript's cost without its benefit. Use any as a temporary placeholder only, with a comment: // TODO: type this properly.
  • Not running the tests after renaming. TypeScript can compile successfully and still have a logic change — particularly if a CommonJS-to-ESM conversion changes how default exports work. Run the test suite after each rename to confirm runtime behaviour is unchanged.

🎯 Practice task

Rename your first file.

  1. Find a utility file in your JavaScript test project that meets the criteria: leaf node, under 100 lines, well-understood. If you're using the project from JavaScript for QA, pick the date formatter or URL builder helper.
  2. Run git mv path/to/file.js path/to/file.ts.
  3. Run npm run type-check and list all errors.
  4. Fix each error: add parameter types, convert require/module.exports to import/export, add return types.
  5. Run npm run type-check again — zero errors.
  6. Run your test suite. Confirm everything still passes.
  7. Commit: git commit -m "Migrate <filename>.js to TypeScript".

The next lesson covers function signatures in depth — because typing function parameters and return values is the single highest-value act in a migration.

// tip to track lessons you complete and pick up where you left off across devices.