TypeScript 6.0 `--checkJs` With Full Strict Mode: Migrating Mixed JS/TS Repos Without a Flag Day
Master TypeScript 6.0's enhanced `--checkJs` to incrementally enforce strict mode on JavaScript files without breaking CI or forcing a big-bang migration.
Most TypeScript migration failures stem from attempting a full codebase conversion in one pull request. Teams either rewrite thousands of lines simultaneously and break CI for weeks, or they abandon strict type checking entirely because the surface area looks too large. The result is a half-migrated codebase that gets no value from TypeScript's safety guarantees.
TypeScript 6.0 changes this calculus with enhanced --checkJs support that applies full strict mode to JavaScript files without requiring a .ts extension. The old approach forced teams to choose between loose type checking via JSDoc comments or a complete file-by-file rename. Neither option supported incremental adoption of strict mode on existing JavaScript.
The broken pattern looks like this: a team enables allowJs: true to permit mixed JS/TS, but JavaScript files remain unchecked unless developers manually add // @ts-check comments to each file. Those comments activate basic type checking but cannot enforce strictNullChecks or noImplicitAny without additional configuration gymnastics. The team ends up with three tiers of safety: strict TypeScript files, loosely-checked JavaScript with @ts-check, and completely unchecked JavaScript without the comment. This inconsistency undermines the entire migration.
%% alt: Problem flow showing CI breaking from flag day migration attempt
flowchart LR
A("Mixed JS/TS codebase") --> B("Attempt flag day migration")
B --> C("Rename all .js to .ts")
C --> D("CI fails with 2000 errors")
D --> E("Team reverts changes")
style D stroke:#ef4444,fill:#450a0a,color:#fca5a5
style E stroke:#ef4444,fill:#450a0a,color:#fca5a5
TypeScript 6.0 addresses this with a --checkJs flag that applies the exact same strict mode rules to .js files as .ts files when configured properly. Teams can enable strict null checks and implicit any detection on JavaScript without renaming files. The migration becomes a configuration change rather than a mass file operation.
%% alt: Solution flow showing incremental strict mode adoption on JavaScript files
flowchart LR
A("Mixed JS/TS codebase") --> B("Enable checkJs with strict mode")
B --> C("Fix errors module by module")
C --> D("CI stays green throughout")
D --> E("Gradual migration to .ts optional")
style D stroke:#34d399,fill:#0b3b2e,color:#d1fae5
style E stroke:#34d399,fill:#0b3b2e,color:#d1fae5
style C stroke:#c084fc,fill:#3b0764,color:#f3e8ff,stroke-width:4px
This article covers the exact configuration and phased strategy to migrate a mixed JavaScript/TypeScript codebase to full strict mode without a flag day. The approach maintains green CI throughout and allows teams to ship features while progressively tightening type safety.
Key Takeaways
- TypeScript 6.0
--checkJsapplies full strict mode to JavaScript files without requiring.tsextensions, eliminating the need for mass file renames. - A phased migration using
excludepatterns allows teams to enable strict checking on JavaScript incrementally while keeping CI green. - The module-by-module strategy fixes one isolated unit at a time, shipping features continuously rather than blocking on a complete rewrite.
--checkJswith strict mode detects the same null pointer errors and implicit any violations in JavaScript as it does in TypeScript, creating uniform safety across the codebase.- This migration path avoids the three-tier safety problem where some files are strict, some have
@ts-check, and some remain completely unchecked.
Understanding --checkJs With Full Strict Mode in TypeScript 6.0
The --checkJs flag instructs the TypeScript compiler to analyze JavaScript files using the same type checker that processes TypeScript. Prior to TypeScript 6.0, this flag activated basic type checking but did not honor strict mode flags unless developers added explicit @ts-check comments and additional JSDoc annotations. The result was a weaker safety tier for JavaScript files.
TypeScript 6.0 changes this behavior. When both --checkJs and --strict are enabled in tsconfig.json, the compiler enforces strict null checks, implicit any detection, and all other strict mode rules on .js files exactly as it does on .ts files. This distinction is critical. A JavaScript function that returns a potentially null value now triggers the same error as an equivalent TypeScript function.
The implication here is that teams no longer need to rename files to .ts to get strict type safety. They can progressively enable strict mode on JavaScript modules, fix the errors, and leave the .js extension intact. The file extension becomes an implementation detail rather than a determinant of type safety.
%% alt: Hierarchy showing checkJs applying strict mode to JavaScript files
flowchart TD
A("tsconfig.json with strict: true") --> B("TypeScript files (.ts)")
A --> C("JavaScript files (.js)")
B --> D("Strict null checks enforced")
C --> E("No checking (TypeScript 5.x)")
C --> F("Full strict mode (TypeScript 6.0 with checkJs)")
F --> D
style E stroke:#fbbf24,fill:#3a2f0b,color:#fef3c7
style F stroke:#c084fc,fill:#3b0764,color:#f3e8ff,stroke-width:4px
style D stroke:#34d399,fill:#0b3b2e,color:#d1fae5
The practical benefit surfaces when a team inherits a large JavaScript codebase and wants to add type safety without halting feature development. Renaming hundreds of files in one commit creates merge conflicts and breaks every import statement. Enabling --checkJs with strict mode allows the team to fix type errors in targeted modules while leaving the rest of the codebase unchanged.
This pattern also applies to libraries that ship both JavaScript and TypeScript entry points. A library can maintain .js source files for compatibility reasons while still enforcing strict type safety during development. The compiled output remains JavaScript, but the development experience matches TypeScript's safety guarantees.
The failure mode here is subtle but expensive. Teams that enable --checkJs without --strict get type checking on JavaScript files, but they do not get null safety or implicit any detection. A function that accepts undefined as a parameter silently passes type checking, and the team believes they have strict mode when they do not. Always enable both flags together to avoid this false sense of security.
Setting Up Incremental Migration: tsconfig.json Strategy
The first step in a mixed-codebase migration is configuring tsconfig.json to apply strict mode only to files ready for checking. The naive approach enables strict: true and checkJs: true globally, which immediately breaks CI with hundreds of errors. The correct strategy uses the exclude field to carve out unchecked modules and progressively shrinks that exclusion list.
Start with a configuration that enables strict mode for TypeScript files only:
{
"compilerOptions": {
"strict": true,
"allowJs": true,
"checkJs": false,
"noEmit": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}This baseline configuration compiles TypeScript with strict mode while permitting JavaScript imports via allowJs: true. The checkJs: false setting means JavaScript files are imported but not type-checked. CI remains green because no new errors surface.
The next phase enables checkJs: true but excludes all JavaScript files initially:
{
"compilerOptions": {
"strict": true,
"allowJs": true,
"checkJs": true,
"noEmit": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"],
"exclude": [
"node_modules",
"src/**/*.js"
]
}This configuration sets the foundation for incremental migration. The checkJs: true flag is active, but the exclude pattern prevents it from applying to any JavaScript file. The team can now remove individual files from the exclusion list as they fix type errors.
%% alt: Flow showing progressive exclusion list reduction during migration
flowchart LR
A("Enable checkJs globally") --> B("Exclude all .js files")
B --> C("Remove one module from exclude")
C --> D("Fix strict mode errors")
D --> E("Repeat for next module")
style C stroke:#c084fc,fill:#3b0764,color:#f3e8ff,stroke-width:4px
style D stroke:#7c9cf0,fill:#142544,color:#eaf2ff
style E stroke:#34d399,fill:#0b3b2e,color:#d1fae5
The advantage of this approach is that each module migration is an isolated change. A developer picks a JavaScript file, removes it from the exclusion list, runs the type checker, and fixes the errors. The pull request contains only the fixes for that one module. Other team members can continue shipping features in unchecked JavaScript files without conflict.
For teams with deeply nested directory structures, the exclusion pattern can target specific folders rather than individual files:
{
"exclude": [
"node_modules",
"src/legacy/**/*.js",
"src/utils/**/*.js"
]
}This pattern allows the team to migrate entire feature areas in order. The src/components folder might be fully strict-checked while src/legacy remains unchecked. As each folder completes migration, its exclusion line is deleted.
The failure mode here is forgetting to document the migration strategy in the project README. New team members see checkJs: true in the configuration and assume all JavaScript is checked. They add new .js files without realizing those files are excluded. Always maintain a migration checklist that shows which modules are checked and which remain in the exclusion list.
Code Example: Enabling Strict Checks on JavaScript Files Selectively
A concrete example demonstrates how --checkJs with strict mode catches errors in JavaScript that would previously pass type checking. Consider a utility module that fetches user data from an API:
// src/api/users.js (before migration)
export function getUserById(id) {
const response = fetch(`/api/users/${id}`);
return response.json();
}
export function getUserName(user) {
return user.name.toUpperCase();
}This code compiles without errors when checkJs: false. The type checker does not analyze JavaScript files, so it ignores the missing await keyword and the potential null pointer in getUserName.
When the team removes src/api/users.js from the exclusion list and runs the type checker, TypeScript 6.0 with checkJs: true and strict: true reports two errors:
src/api/users.js:3:18 - error TS2322: Type 'Promise<Response>' is not assignable to type 'Response'.
const response = fetch(`/api/users/${id}`);
src/api/users.js:8:10 - error TS18047: 'user' is possibly 'null'.
return user.name.toUpperCase();
The first error catches the missing await. The second error catches the null pointer when user is undefined. These are the exact same errors TypeScript would report for a .ts file under strict mode.
The corrected version adds explicit types via JSDoc and handles null cases:
// src/api/users.js (after migration)
/**
* @param {string} id
* @returns {Promise<{name: string, email: string}>}
*/
export async function getUserById(id) {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
/**
* @param {{name: string} | null} user
* @returns {string}
*/
export function getUserName(user) {
if (user === null) {
return 'Unknown';
}
return user.name.toUpperCase();
}The JSDoc annotations provide explicit type information that the TypeScript compiler uses to enforce strict mode. The async keyword fixes the promise handling, and the null check prevents the runtime error.
This example shows the value proposition of --checkJs with strict mode. The team did not rename the file to .ts, but they achieved the same type safety. The migration cost was adding JSDoc comments and fixing two real bugs. The alternative approach of ignoring type safety or adding a weak @ts-check comment would have missed both errors.
For teams concerned about the verbosity of JSDoc, TypeScript 6.0 also supports inferred types from initialization values. A simpler pattern for local functions is to rely on inference:
// src/utils/format.js
export function formatCurrency(amount) {
if (typeof amount !== 'number') {
return '$0.00';
}
return `$${amount.toFixed(2)}`;
}When checkJs: true and strict: true are enabled, TypeScript infers that amount has type number | undefined from the runtime check. The function returns a string in all branches. No explicit JSDoc is required.
The implication here is that teams can mix explicit JSDoc for complex types with inferred types for simple utility functions. The type safety is uniform regardless of annotation style.
Phased Migration: From allowJs to Full Strict Without Breaking CI
The phased migration strategy breaks the transition into three discrete stages, each of which maintains green CI and allows feature development to continue. This approach eliminates the flag day problem where the entire team stops shipping features to focus on migration.
Phase one establishes the baseline configuration with allowJs: true and checkJs: false. All existing TypeScript files compile with strict mode, and JavaScript files are permitted as imports but not type-checked. This phase typically requires zero code changes because the configuration matches most teams' current setup.
Phase two enables checkJs: true with a global exclusion of all JavaScript files. The configuration change activates the flag, but the exclusion list prevents any new errors from surfacing. Teams commit this change as a single pull request that updates tsconfig.json and documents the migration plan in the README.
Phase three is the iterative work. Each pull request removes one module from the exclusion list, fixes the strict mode errors in that module, and commits the changes. The exclusion list shrinks over time until it reaches zero entries. At that point, the team deletes the exclusion pattern entirely.
%% alt: Flow showing three-phase migration from allowJs to full strict mode
flowchart LR
A("Phase 1: allowJs, checkJs off") --> B("Phase 2: checkJs on, all JS excluded")
B --> C("Phase 3: incremental exclusion removal")
C --> D("Zero exclusions, full strict mode")
style C stroke:#c084fc,fill:#3b0764,color:#f3e8ff,stroke-width:4px
style D stroke:#34d399,fill:#0b3b2e,color:#d1fae5
The critical detail in phase three is picking modules in dependency order. Leaf modules with no internal dependencies migrate first. Shared utility modules migrate after their consumers are fixed. This ordering minimizes cascading type errors where a single unfixed module breaks type checking in ten dependents.
For example, a team might migrate in this order:
- Pure utility functions with no dependencies:
src/utils/string.js,src/utils/array.js - API client modules that depend on utilities:
src/api/users.js,src/api/products.js - React components that depend on API clients:
src/components/UserList.js - Page components that depend on child components:
src/pages/Dashboard.js
This order ensures that when a developer removes src/components/UserList.js from the exclusion list, the src/api/users.js module it imports is already fully typed. The type errors in UserList.js are localized to that file rather than propagating from unfixed dependencies.
The failure mode here is attempting to migrate modules in alphabetical order or random order without considering dependencies. A developer removes src/pages/Dashboard.js from the exclusion list before fixing src/components/UserList.js, and TypeScript reports fifty errors in the dashboard because it cannot infer types from the unchecked child component. The developer becomes frustrated and re-adds the dashboard to the exclusion list. Always map dependencies before starting phase three.
Comparison: @ts-check Comments vs --checkJs vs Full TypeScript Migration
Teams approaching a mixed-codebase migration face three distinct strategies. Each has different tradeoffs in terms of safety, migration cost, and ongoing maintenance burden. The choice depends on the team's timeline and tolerance for incomplete type coverage.
The first option is adding // @ts-check comments to individual JavaScript files. This approach activates type checking on a per-file basis without changing tsconfig.json. The comment enables basic type inference and JSDoc validation, but it does not enforce strict mode flags like strictNullChecks unless additional configuration work is done.
The second option is enabling --checkJs with strict mode at the configuration level and using exclusion lists to control which files are checked. This approach applies uniform strict mode to all checked JavaScript files and allows incremental migration without per-file comments.
The third option is renaming files from .js to .ts and migrating to full TypeScript syntax. This approach provides the strongest type safety and eliminates the need for JSDoc comments, but it requires changing every import statement and is the most disruptive to ongoing development.
%% alt: Comparison of three migration strategies with different outcomes
flowchart LR
subgraph tscheck ["@ts-check comments"]
A("Per-file opt-in") --> B("Basic type checking")
B --> C("No strict mode enforcement")
end
subgraph checkjs ["--checkJs with strict"]
D("Configuration-level control") --> E("Full strict mode")
E --> F("Incremental via exclusions")
end
style C stroke:#fbbf24,fill:#3a2f0b,color:#fef3c7
style F stroke:#34d399,fill:#0b3b2e,color:#d1fae5
The @ts-check strategy works for small codebases with a handful of JavaScript files. A team can add the comment to five utility modules and get basic type safety without a comprehensive migration plan. The limitation is that this approach does not scale. As the checked file count grows, maintaining per-file comments becomes error-prone. New JavaScript files are added without comments, and the type safety coverage regresses.
The --checkJs with strict mode strategy works for medium to large codebases where the team wants uniform type safety across all checked files. The exclusion list provides a clear migration boundary, and the strict mode enforcement catches real bugs. The limitation is that the team must maintain the exclusion list and ensure new JavaScript files are not accidentally excluded.
The full TypeScript migration strategy works for greenfield projects or codebases where the team has allocated dedicated migration time. Renaming files to .ts eliminates the need for JSDoc and provides the best development experience. The limitation is that this approach is the most disruptive and has the highest upfront cost.
The practical recommendation for most teams is the --checkJs with strict mode strategy. It provides the same type safety as full TypeScript migration without the file rename overhead. Teams that later decide to rename files to .ts can do so incrementally without changing type annotations because the JSDoc comments are already in place.
Real-World Migration Path: Module-by-Module Strategy
A concrete migration path for a typical React application demonstrates how the phased strategy plays out in production. Consider a codebase with the following structure:
src/
utils/
format.js
validate.js
api/
client.js
users.js
components/
Button.js
UserCard.js
pages/
Dashboard.js
The migration begins by enabling checkJs: true and excluding all JavaScript files. The initial tsconfig.json looks like this:
{
"compilerOptions": {
"strict": true,
"allowJs": true,
"checkJs": true,
"jsx": "react",
"noEmit": true
},
"include": ["src/**/*"],
"exclude": [
"node_modules",
"src/**/*.js"
]
}Week one focuses on utility modules because they have no internal dependencies. A developer removes src/utils/format.js and src/utils/validate.js from the exclusion list:
{
"exclude": [
"node_modules",
"src/api/**/*.js",
"src/components/**/*.js",
"src/pages/**/*.js"
]
}The type checker reports errors in both utility files. The developer adds JSDoc annotations and fixes null handling. CI stays green because no other modules are affected. The pull request ships and the team moves to the next layer.
Week two targets API client modules. The developer removes src/api/client.js and src/api/users.js from the exclusion list. These modules import the utility functions that were fixed in week one, so the type checker can infer parameter and return types. The developer fixes async handling and null checks, and the pull request ships.
%% alt: Module-by-module migration flow showing dependency order
flowchart LR
A("Week 1: utils modules") --> B("Week 2: api modules")
B --> C("Week 3: components modules")
C --> D("Week 4: pages modules")
D --> E("Zero exclusions achieved")
style E stroke:#34d399,fill:#0b3b2e,color:#d1fae5
style A stroke:#c084fc,fill:#3b0764,color:#f3e8ff,stroke-width:4px
Week three focuses on React components. The developer removes src/components/Button.js and src/components/UserCard.js from the exclusion list. These components import API clients that are now fully typed, so prop types are inferred from usage. The developer adds explicit JSDoc for component props and fixes event handler types.
Week four completes the migration with page components. The developer removes src/pages/Dashboard.js from the exclusion list. The dashboard imports fully typed child components and API clients, so most type errors are caught by inference. The developer fixes a few remaining null checks and the exclusion list is empty.
The final tsconfig.json looks like this:
{
"compilerOptions": {
"strict": true,
"allowJs": true,
"checkJs": true,
"jsx": "react",
"noEmit": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}The entire codebase is now under strict mode type checking. The team shipped features every week during the migration. CI stayed green throughout. The migration cost was four weeks of focused work spread across the team rather than a multi-month freeze on feature development.
This pattern scales to larger codebases by extending the timeline. A codebase with fifty modules might take twelve weeks instead of four, but the principle remains the same. Pick modules in dependency order, fix one at a time, and keep CI green.
Frequently Asked Questions
Does --checkJs with strict mode catch the same errors as TypeScript files?
Yes. When both checkJs: true and strict: true are enabled in TypeScript 6.0, the compiler applies identical strict mode rules to JavaScript and TypeScript files. A null pointer error or implicit any violation triggers the same error message regardless of file extension.
Can teams use --checkJs on a codebase with existing @ts-check comments?
Yes. The @ts-check comments and --checkJs flag can coexist. Files with @ts-check are checked even when they are in the exclusion list. Once the team enables --checkJs globally, the per-file comments become redundant and can be removed.
How long does a typical migration take for a medium-sized codebase?
A codebase with fifty to one hundred modules typically completes migration in eight to twelve weeks when following the module-by-module strategy. Teams that dedicate one developer per week to migration work can process five to ten modules weekly depending on complexity.
What happens to JavaScript files that remain excluded when the team adds new features?
New JavaScript files added to excluded folders bypass strict mode checking until they are explicitly removed from the exclusion list. Teams should document the exclusion list in the README and require new code to be added outside excluded folders or migrate excluded modules before adding features.
Is it worth migrating JavaScript files to TypeScript syntax after enabling --checkJs?
The decision depends on team preference. Once JavaScript files have full strict mode type safety via JSDoc, the functional benefit of renaming to .ts is minimal. Teams that prefer TypeScript syntax for developer experience can rename files incrementally without losing type safety during the transition.
Conclusion: Avoiding the Flag Day and Maintaining Team Velocity
The TypeScript 6.0 enhancement to --checkJs eliminates the forced choice between a disruptive flag day migration and incomplete type safety. Teams can apply full strict mode to JavaScript files incrementally using exclusion lists and module-by-module migration. This approach maintains green CI throughout, allows continuous feature shipping, and achieves the same type safety as a full TypeScript migration without renaming files.
The critical steps are enabling checkJs: true with strict: true, excluding all JavaScript files initially, and removing modules from the exclusion list in dependency order. Each module migration is an isolated pull request that fixes strict mode errors in one unit. The exclusion list shrinks over time until the entire codebase is checked.
That covers the essential patterns for migrating mixed JavaScript/TypeScript repositories to full strict mode. Apply these in production and the difference will be immediate.