JavaScript & TypeScript Integration
Most real-world engineering teams do not write new applications from a completely blank canvas. Instead, developers frequently work with existing, mature JavaScript codebases containing hundreds of thousands of lines of code. Attempting to convert an entire legacy codebase to TypeScript overnight in a single massive pull request is almost always a recipe for disaster.
TypeScript was specifically engineered for Gradual Adoption. You can introduce TypeScript incrementally into an existing JavaScript project, type-check vanilla JavaScript files using JSDoc annotations, configure allowJs and checkJs, and migrate files one-by-one with zero downtime.
┌────────────────────────────────────────────────────────────┐
│ Gradual Migration Pipeline │
├────────────────────────────────────────────────────────────┤
│ Step 1: Add tsconfig.json with "allowJs": true │
│ (Compile existing .js files with tsc) │
│ │
│ Step 2: Enable "checkJs": true & annotate with JSDoc │
│ (Gain type checking inside .js without renaming) │
│ │
│ Step 3: Rename leaf utility files (.js ──> .ts) │
│ (Add strict interfaces, generics, and types) │
│ │
│ Step 4: Migrate complex modules and components │
│ │
│ Step 5: Turn on "strict": true & remove "allowJs" │
└────────────────────────────────────────────────────────────┘
Configuring allowJs and checkJs
To begin using TypeScript in an existing JavaScript project, install TypeScript and create a tsconfig.json enabling allowJs:
allowJs: true: Allows JavaScript (.js,.jsx) files to be imported and compiled alongside TypeScript files.checkJs: true: Instructs the TypeScript compiler to perform static type checking on vanilla.jsfiles using type inference and JSDoc comments.
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"allowJs": true,
"checkJs": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"]
}
If you only want type checking on specific .js files without enabling checkJs globally across the entire project, you can add // @ts-check as the very first line of any individual JavaScript file:
// @ts-check
// This vanilla JS file is now type-checked by TypeScript!
let port = 8080;
// Error: Type 'string' is not assignable to type 'number'.
port = "8080";
Conversely, if a legacy .js or .ts file contains too many errors to fix immediately, you can suppress checks on that file with // @ts-nocheck or suppress a single line with // @ts-expect-error.
Type Annotations with JSDoc
TypeScript has full native support for JSDoc type annotations. In vanilla .js files where TypeScript syntax (: string, interface) cannot be written directly, JSDoc comments provide 100% of TypeScript's type checking capabilities:
// @ts-check
/**
* Calculates discount price.
* @param {number} originalPrice - The starting price before discount
* @param {number} [discountPercentage=10] - Optional discount percentage
* @returns {number} The final calculated price
*/
function calculateDiscount(originalPrice, discountPercentage = 10) {
return originalPrice * (1 - discountPercentage / 100);
}
calculateDiscount(100, 20); // Valid
// Error: Argument of type 'string' is not assignable to parameter of type 'number'.
calculateDiscount("100");
Defining Complex Types with @typedef and @callback
You can define reusable interfaces, object shapes, and callback types inside JSDoc comments:
// @ts-check
/**
* @typedef {Object} DatabaseConfig
* @property {string} host
* @property {number} port
* @property {boolean} [ssl]
*/
/**
* @callback ConnectionCallback
* @param {Error | null} err
* @param {string} connectionId
* @returns {void}
*/
/**
* @param {DatabaseConfig} config
* @param {ConnectionCallback} callback
*/
function connectDatabase(config, callback) {
// TypeScript checks that 'config.host' exists and is a string
console.log(`Connecting to ${config.host}:${config.port}`);
callback(null, "conn_123");
}
Importing TypeScript Types into JavaScript Files
You can even import TypeScript interfaces directly into vanilla JavaScript JSDoc comments:
// @ts-check
/**
* @param {import('./types').UserProfile} profile
*/
function displayProfile(profile) {
console.log(profile.username);
}
Step-by-Step Migration Strategy (JS → TS)
When migrating a large production repository, follow this battle-tested phased strategy:
Phase 1: Setup & Pipeline
- Install TypeScript and configure
tsconfig.jsonwithallowJs: trueandstrict: false. - Configure build scripts and CI pipelines to run
tsc --noEmiton every pull request.
Phase 2: Migrate Leaf Dependencies
Migrate files from the bottom of the dependency graph upwards:
- Start with pure utility functions (
src/utils/math.js→src/utils/math.ts). - Rename constants, configuration files, and helper libraries.
- Add explicit interfaces for all shared data structures.
Phase 3: Migrate Core Services and Components
- Convert API client files, repository classes, and state management stores.
- Rename UI components (
.jsx→.tsx). - Replace loose
anyvalues with explicit domain models.
Phase 4: Enable Strictness Flags
- Incrementally turn on strictness compiler flags one by one:
- First:
noImplicitAny: true - Second:
strictNullChecks: true - Third:
strict: true
- First:
- Remove
allowJs: trueonce all.jsfiles are converted.
Summary
- TypeScript supports gradual adoption through
allowJs: trueandcheckJs: true. // @ts-checkenables per-file type checking on individual legacy JavaScript files.- JSDoc comments (
@param,@returns,@typedef,@type) provide full static typing inside standard.jsfiles without a compilation step. - Migration should proceed from leaf utilities up to high-level modules, ensuring continuous stability.
- Strict mode flags should be enabled incrementally as the codebase reaches full
.tscoverage.
Best Practices
- Migrate Leaf Utilities First: Begin conversion on isolated files with no dependencies before tackling core business logic.
- Use JSDoc for Zero-Build JS Projects: If a project cannot use a build tool or compiler step, use JSDoc +
// @ts-checkfor instant TypeScript validation. - Prefer
// @ts-expect-errorover// @ts-ignore: If you must suppress a legacy error temporarily,@ts-expect-errorensures that if the error is fixed later, the compiler will alert you to delete the suppression comment. - Automate Type Checking in CI: Run
tsc --noEmiton CI pull requests to prevent regressions during the migration period.