Common JavaScript to TypeScript Migration Mistakes

Avoid the most common mistakes when migrating JavaScript to TypeScript. Learn what goes wrong, why it happens, and how to fix each issue during migration.

6 min read

These TypeScript migration mistakes show up in nearly every JavaScript-to-TypeScript migration. They are predictable, and knowing them in advance saves hours of frustration. Here are the most common ones and how to avoid them.

1. Enabling Strict Mode Too Early

Full strict mode assumes a codebase that was written with types in mind from the start. Turning it on before any file has been converted floods you with more errors than any team can realistically work through at once.

The mistake: turning on strict: true on day one of the migration.

jsonjson
{
  "compilerOptions": {
    "strict": true
  }
}

Running tsc on an untyped JavaScript codebase with full strict mode produces hundreds of errors. Parameter types, null checks, implicit any, and function type mismatches all surface at once. It is overwhelming and discouraging.

The fix: Start with strict mode off. Convert files incrementally. Enable strict checks one at a time:

  • First, convert files and fix basic errors.
  • Enable the check that flags unannotated parameters, and fix those errors.
  • Enable null and undefined checking, and fix those issues.
  • Finally, enable full strict mode when the codebase is clean.

For the step-by-step process, see Migrate JavaScript to TypeScript Step by Step.

2. Forgetting to Set outDir

The mistake: running tsc without setting outDir, which causes TypeScript to output compiled JavaScript files into the same directory as your source TypeScript files. Your source files get overwritten or mixed with compiled output.

jsonjson
{
  "compilerOptions": {
    "allowJs": true
  }
}

With this config, a source file compiles to a JavaScript file right next to it, silently overwriting your original JavaScript.

The fix: Always separate the source directory from the compiled output directory:

jsonjson
{
  "compilerOptions": {
    "outDir": "./dist",
    "rootDir": "./src"
  }
}

Source files stay in the source folder. Compiled output goes to the output folder. No overwrites, no confusion.

3. Overusing any Instead of unknown

The mistake: using any everywhere because it is the quickest way to make errors disappear:

typescripttypescript
function handleApiResponse(data: any): any {
  return data.results.map((item: any) => item.name);
}

This compiles but gives you zero type safety. A typo in either property name goes undetected until runtime.

The fix: Use unknown for values whose type you genuinely do not know, and check the shape before reading from it:

typescripttypescript
function handleApiResponse(data: unknown): string[] {
  if (
    typeof data === "object" &&
    data !== null &&
    "results" in data &&
    Array.isArray(data.results)
  ) {
    return data.results.map(item => getItemName(item));
  }
  return [];
}

Pulling the per-item logic into its own helper keeps the outer function focused on the overall shape, and each function stays small enough to read at a glance:

typescripttypescript
function getItemName(item: unknown): string {
  if (typeof item === "object" && item !== null && "name" in item) {
    return (item as { name: string }).name;
  }
  return "unknown";
}

unknown preserves the uncertainty signal while forcing you to validate the shape. For production code, a validation library is cleaner than manual checks. See Handle Dynamic Values in TypeScript.

4. Renaming Every File at Once

The mistake: batch-renaming every JavaScript file to TypeScript at the same time and then trying to fix every error at once.

This creates a situation where nothing compiles, errors cascade across the entire project, and you have no working baseline to test against.

The fix: Rename one file at a time, starting with leaf modules that have no dependencies on other project files:

bashbash
mv src/utils/formatPrice.js src/utils/formatPrice.ts
npx tsc
# Fix errors in formatPrice.ts before moving on

After each file compiles cleanly, commit the change and move to the next file. Your project remains runnable at every step.

5. Not Installing @types for Dependencies

The mistake: getting a "Cannot find module" error for a package like express and writing manual declaration files for every dependency.

typescripttypescript
// Manual workaround (waste of time)
declare module "express" {
  const express: any;
  export default express;
}

This works but you lose all type information for the library, and every method call on it falls back to loose, unchecked typing.

The fix: Install the corresponding @types package instead of writing declarations by hand:

bashbash
npm install --save-dev @types/express

Most popular npm packages have types available on DefinitelyTyped. For packages without a types package, create a minimal declaration file in one place and share it. See Install and Use @types Packages.

6. Abusing Type Assertions

The mistake: using as to force types without any runtime validation:

typescripttypescript
const user = JSON.parse(rawUserData) as User;
user.email.toLowerCase();

If the raw data is an empty object or has a different shape, the email property is undefined at runtime. Calling a string method on undefined throws a runtime error that TypeScript did not catch because you overrode its checks with the assertion.

The fix: Validate at the boundary. For simple cases, narrow with a runtime check written as a type guard:

typescripttypescript
function isUser(value: unknown): value is User {
  return (
    typeof value === "object" &&
    value !== null &&
    "email" in value &&
    typeof (value as User).email === "string"
  );
}

Calling the guard before using the parsed value replaces the blind assertion with an actual check. For complex data, a validation library is usually less code to maintain than guards like this one:

typescripttypescript
const raw: unknown = JSON.parse(rawUserData);
 
if (isUser(raw)) {
  raw.email.toLowerCase();
} else {
  throw new Error("Invalid user data");
}

7. Ignoring Null and Undefined

The mistake: writing TypeScript as if values are always present, the same way JavaScript lets you:

typescripttypescript
function getUserName(id: number): string {
  const user = users.find(u => u.id === id);
  return user.name;
}

With strictNullChecks on, this fails to compile because the array's find method returns either the item or undefined, and the item might genuinely not exist in the array at all:

texttext
Object is possibly 'undefined'.

Accessing a property on undefined would be a runtime crash if this compiled. The compiler is catching a real bug before it ships.

The fix: Handle the missing case explicitly instead of assuming the lookup always succeeds:

typescripttypescript
function getUserName(id: number): string {
  const user = users.find(u => u.id === id);
  if (!user) {
    throw new Error(`User ${id} not found`);
  }
  return user.name;
}

After enabling strictNullChecks, TypeScript flags every potentially null or undefined access. Fix them systematically. This single setting prevents the most common runtime error in JavaScript.

8. Keeping JavaScript-Style Dynamic Patterns

The mistake: keeping patterns that work in JavaScript but are hard to type safely:

javascriptjavascript
// Adding properties to objects after creation
const config = {};
config.port = 3000;
config.host = "localhost";
 
// Using arguments instead of rest parameters
function sum() {
  let total = 0;
  for (let i = 0; i < arguments.length; i++) {
    total += arguments[i];
  }
  return total;
}

The fix: Adopt TypeScript-friendly patterns during migration. Declare the object shape upfront instead of building it up property by property, and use rest parameters instead of the legacy arguments object:

typescripttypescript
// Declare the full shape upfront
const config = {
  port: 3000,
  host: "localhost",
};
 
// Use rest parameters
function sum(...values: number[]): number {
  return values.reduce((total, n) => total + n, 0);
}

These changes are small but make the code easier to type and read. For functions with complex argument patterns, see Type Existing JavaScript Functions.

9. Not Testing After Each Batch

The mistake: making dozens of type-related changes and then discovering at the end that the application is broken.

Adding type annotations does not change runtime behavior. But fixing type errors sometimes means adding null checks, restructuring objects, or changing function signatures. These changes can introduce bugs.

The fix: Run your test suite after every batch of converted files:

bashbash
npm test

If you do not have tests, run the application manually and verify the key features. The compiler guarantees type safety; it does not guarantee logical correctness.

10. Giving Up on a File Because It Has Too Many Errors

The mistake: hitting a file with dozens of type errors, deciding it is too hard, and adding // @ts-nocheck at the top to skip it permanently.

Every skipped file is a gap in your type safety. Bugs in that file will not be caught, and code that imports from it gets weaker types.

The fix: If a file is genuinely too hard to convert right now, use the comment as a temporary measure and schedule it for later. Keep a list of skipped files and revisit them when the rest of the codebase is typed. A skipped file today is a migration task for next week, not a permanent decision.

Rune AI

Rune AI

Key Insights

  • Do not enable strict mode on day one. Start lenient and tighten gradually.
  • Always set outDir to prevent TypeScript from overwriting source files.
  • Use unknown instead of any for genuinely dynamic values.
  • Do not rename every file at once. Convert files incrementally.
  • Install @types packages for your dependencies instead of writing manual declarations.
  • Test after each batch of converted files to catch behavior changes.
RunePowered by Rune AI

Frequently Asked Questions

What is the single biggest mistake during migration?

Enabling strict mode too early. A codebase without types will produce hundreds or thousands of errors under full strict mode. Start with strict: false, convert files, then enable strict checks one at a time.

Should I convert every file before running the project?

No. TypeScript supports partial migration. Convert a few files, run the compiler, test the app, and repeat. Your project should work at every step of the migration.

Is it a mistake to keep any in my codebase?

Keeping any permanently defeats the purpose of TypeScript. Use any as a temporary migration tool, then replace it with unknown or specific types. Track remaining any usages with a lint rule.

Conclusion

Migration mistakes are predictable and avoidable. Start lenient, tighten gradually, always set outDir, use unknown instead of any, and test after every batch of changes. The compiler is your ally. Work with it, not against it.