JS to TypeScript Migration
Since TypeScript is purposefully and rigorously a superset of JavaScript, a key component of adopting the language is the process of working with existing JavaScript (JS) code and gradually moving a codebase to TypeScript (TS). Any legitimate.js file can be renamed to a .ts
file and still compile to valid JS that is equal to the original thanks to this design philosophy.
Because developers write JavaScript, TypeScript is designed to be practical and offers tools to take this reality into account. Configuring the TypeScript Compiler (TSC) using tsconfig.json
and applying type checking selectively are the two main steps in the migration process.
Gradual Migration Strategies Overview
When migrating a legacy codebase, you generally aim to start with small islands of TypeScript safety within a larger untyped system. The migration strategy typically consists of the following steps: Add a tsconfig.json
, allow the compilation of JavaScript files (using allowJs
and/or checkJs
), change code file extensions from .js
to .ts
, and start suppressing errors using any. You then write new code in TypeScript, making minimal use of any
, and later revisit the old code to add explicit type annotations and fix bugs.
Adding
Creating a tsconfig.json
file to establish a TypeScript project root is the first step in any conversion. When this file is present, the directory is recognised by the TypeScript compiler (TSC) and integrated development environments (IDEs) as the root of a project that uses TypeScript.
An empty JSON object {}
can serve as a minimum tsconfig.json
, instructing TSC to use sane default compiler choices and include all .ts
files (and subdirectories) in the compilation environment. An alternative is to create a file with configuration parameters using the tsc --init
command.
CompilerOptions
, which specify how the compiler should interpret and output JavaScript, are stored in tsconfig.json
. The configuration for working with pre-existing JavaScript is defined in this file.
Interoperating with JavaScript using Compiler Options
Certain compiler parameters must be enabled in the compilerOptions
section of the tsconfig.json
file in order to support a codebase that includes a combination of TypeScript and JavaScript files.
Allowing JavaScript Files in the Project
JavaScript files (files with the .js
or .jsx
extension, if the jsx
option is also set) can be included in the compilation and type-checking process when the allowJs
compiler option is enabled.
When allowJs
is set to true
, the TSC will use the configured module system (e.g., CommonJS) to process and transpile the JavaScript files to the designated target ECMAScript version (e.g., ES5). Constructs declared in .js
files can feed into type-checking TypeScript files thanks to this fundamental step, which allows for a mixed codebase. JavaScript files are still permitted and transferred to the output directory if TSC is only used for type checking with the --noEmit
option.
Type-Checking JavaScript Files
Type checking for .js
and .jsx
files is enabled with the checkJs
compiler option, which improves compatibility. When checkJs
is enabled, allowJs
is automatically set to true
if it isn’t already.
TypeScript handles the JavaScript files as though they were regular TypeScript files without TS-specific syntax when checkJs
is enabled. This implies that even before the file is transformed, type errors will be produced by typos in variables, erroneous function calls, and type mismatches .ts
.
You can enable checking for certain JavaScript files by including the // @ts-check
directive as a comment at the top of the file if checkJs
first introduces too many errors across a big codebase. On the other hand, you can use // @ts-nocheck
to exclude noisy files if checkJs
is globally enabled.
Example Configuration:
A configuration enabling both features for gradual migration looks like this:
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"allowJs": true, /* Allow JavaScript files to be compiled/included. */
"checkJs": true, /* Report errors in .js files. */
"noImplicitAny": false /* Temporarily allow implicit 'any' during migration. */
}
}
JSDoc Annotations for Enhanced Type Checking
Because TypeScript’s native type syntax is absent from raw JavaScript, developers can use JSDoc annotations to provide type information to.js files. TypeScript may identify JSDoc definitions and use this structured data as input for its type checker when allowJs
and/or checkJs
are enabled. This is especially helpful for rapidly annotating newly introduced methods to older JavaScript files prior to a complete TypeScript migration.
Developers can provide TypeScript with precise type information by include JSDoc comments like @param
and @returns
. This allows the compiler to infer the correct types when it could otherwise default to any
.
Code Example using JSDoc in a .js file:
Consider a JavaScript file (utils.js
) where we define a function using JSDoc to specify types:
// utils.js
/**
* @param {string} word An input string to convert
* @returns {string} The string in PascalCase
*/
export function toPascalCase(word) {
return word.replace(
/\w+/g,
([a, ...b]) => a.toUpperCase() + b.join('').toLowerCase()
);
}
// Without the JSDoc, TypeScript might infer the argument 'word' as 'any'.
// With JSDoc, the type is correctly inferred as (word: string) => string.
Any improper use of toPascalCase
in other JS files inside the project will now cause a type error detected by TSC if checkJs
is enabled.
Changing Extensions
Renaming files from .js
to .ts
is the main migrating action. Since all JavaScript code is legitimate TypeScript code, the code’s behaviour during runtime won’t be negatively impacted by merely changing the file extension.
But regardless of the checkJs
setting, renaming the file instantly tells TypeScript to use its whole, exacting type-checking mechanism. Usually, this causes a series of diagnostic errors (red squiggly lines in the editor) that reveal type issues, null checks that were overlooked, and implicit assumptions that were used by the original JavaScript code. It’s possible that the codebase is not as “neat” as first thought.
In this phase, the file is converted from checkJs
more relaxed checking rules to a native .ts
file’s more stringent ones.
Suppressing Initial Errors
The objective is frequently to swiftly stabilise the project so that new code may be written safely, postponing the painstaking task of typing old code, when faced with an unexpected profusion of type errors after renaming files to .ts
. This is accomplished by employing the any
type to suppress compilation errors.
It may get around a lot of the type checker by using the any
type, which is essentially an escape hatch that disables type checking for the particular value it is applied to. Although utilising any
to conceal errors is seen as risky, it offers an essential tool to gradually implement TypeScript.
There are two main methods for suppressing errors:
- Type Assertion (
as any
): When a variable is assigned an incompatible type. - Explicit Annotation (
: any
): applied to variables, function parameters, or return types that are hard to deduce or that, if left untyped, would result in numerous errors.
Code Example illustrating error suppression:
Assume a TypeScript file detects an error where a number is assigned to a string variable (a common JavaScript practice that TypeScript flags).
// Original code causing error after .js to .ts rename:
var foo = 123;
var bar = 'hey';
bar = foo; // ERROR: cannot assign a number to a string
The error can be suppressed using a type assertion:
// Suppressing with 'as any' assertion:
var foo = 123;
var bar = 'hey';
bar = foo as any; // Okay!
Alternatively, if a function’s return type is causing issues downstream, it can be explicitly marked as any
:
// Function causing an error downstream:
function foo() {
return 1;
}
var bar = 'hey';
bar = foo(); // ERROR: cannot assign a number to a string
// Suppressing by annotating the function return type:
function foo(): any { // Added `: any`
return 1;
}
var bar = 'hey';
bar = foo(); // Okay!
The developer can temporarily silence the compiler’s protests by carefully using any
, enabling the file to be compiled with markers (like // TODO:
) to be added later and provide strong type definitions. This approach is in line with the “do it fast” migration method, which involves temporarily marking complex types as any
to please the type checker and maintaining loose configuration settings to reduce errors.
With the help of options like allowJs
and checkJs
, as well as the thoughtful application of any
, teams may implement static typing gradually with minimal interruption and instantly take advantage of TypeScript’s tooling and early error detection.