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
prototypemanipulation,argumentsobjects, dynamicrequire()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.tsThen run type-check immediately:
npm run type-checkTypeScript 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:
Parameter 'date' implicitly has an 'any' typeParameter 'format' implicitly has an 'any' typeParameter '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')orformatDate(date, 'yesterday') - A union type
'iso' | 'short' | 'long'that makes the valid formats discoverable - A return type
: stringthat 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 renamefrom the file explorer instead ofgit mv. Most file explorers perform a copy-and-delete rather than a rename — git treats this as a deleted file and a new file, breakinggit log --follow. Always usegit mv. - Fixing errors by adding
: anyeverywhere. If your first converted file has twenty: anyannotations, you've added TypeScript's cost without its benefit. Useanyas 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.
- 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.
- Run
git mv path/to/file.js path/to/file.ts. - Run
npm run type-checkand list all errors. - Fix each error: add parameter types, convert
require/module.exportstoimport/export, add return types. - Run
npm run type-checkagain — zero errors. - Run your test suite. Confirm everything still passes.
- 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.