TypeScript 6.0 `--allowImportingTsExtensions`: What It Unlocks for Monorepo Setups in 2026
The `--allowImportingTsExtensions` flag solves critical path resolution failures in monorepos by permitting explicit `.ts` imports. Learn when to enable it, how it differs from `--rewriteRelativeImportExtensions`, and the bundler compatibility tradeoffs that determine production viability.
Most monorepo import failures stem from a single mismatch: TypeScript forbids .ts extensions in source code, but runtime module resolution often requires them. Teams build elaborate path-mapping workarounds, introduce build-time rewrite steps, or abandon explicit extensions entirely. The result is a fragile setup where a single misconfigured tsconfig.json silently breaks cross-package imports and surfaces only at bundle time.
TypeScript 6.0's --allowImportingTsExtensions eliminates that friction. When enabled, it permits import { fn } from "./utils.ts" in source files without triggering the TS1479 error. The compiler stops enforcing the legacy rule that import specifiers must omit file extensions for non-declaration files. This matters because modern bundlers like Vite and esbuild resolve .ts paths natively, and monorepo package boundaries often demand explicit extensions to avoid Node.js resolution ambiguity.
flowchart LR
A("Developer writes import from './utils.ts'") --> B("TypeScript emits TS1479 error")
B --> C("Build halts, import fails")
style B stroke:#fbbf24,fill:#3a2f0b,color:#fef3c7
style C stroke:#ef4444,fill:#450a0a,color:#fca5a5
The flag does not rewrite imports or emit JavaScript. It strictly removes the compile-time prohibition. Downstream tools (bundlers, loaders, Node.js with custom resolution hooks) handle the actual path resolution. The distinction is critical: --allowImportingTsExtensions unlocks authoring convenience; the runtime environment determines whether those imports execute correctly.
flowchart LR
A("Developer writes import from './utils.ts'") --> B("TypeScript permits the import")
B --> C("Bundler resolves .ts path natively")
C --> D("Build succeeds, runtime executes")
style B stroke:#34d399,fill:#0b3b2e,color:#d1fae5
style D stroke:#34d399,fill:#0b3b2e,color:#d1fae5
This distinction unlocks two immediate benefits for monorepos. First, it eliminates the need for path-rewrite plugins in TypeScript-native build tools. Second, it surfaces resolution failures earlier in the development loop because the compiler no longer masks incorrect paths behind a blanket extension rule. The failure mode shifts from "silent bundler error at production build time" to "immediate feedback during type-checking."
Key Takeaways
--allowImportingTsExtensionspermits.tsextensions in import specifiers without triggering the TS1479 error, but does not rewrite or emit those imports.- Modern bundlers (Vite, esbuild, Turbopack) resolve
.tspaths natively, making the flag viable in production monorepo setups as of 2026. - The flag solves cross-package import failures in monorepos where workspace protocol or explicit extensions are required for correct resolution.
--rewriteRelativeImportExtensionsrewrites.tsto.jsduring emit and requires--noEmitor bundler-only workflows;--allowImportingTsExtensionsdoes not modify output.- Enabling the flag breaks Node.js native execution without custom loaders; reserve it for bundler-driven workflows or use
tsx/ts-nodewith ESM hooks.
The Monorepo Problem: Why Path Resolution Breaks Without It
Monorepo setups introduce a coordination problem between TypeScript's module resolution and the package manager's workspace protocol. A typical workspace import looks like import { api } from "@scope/api/client". TypeScript resolves that path through tsconfig.json paths mappings. The package manager (npm, pnpm, Yarn) resolves it through workspace protocol links in node_modules.
The failure mode appears when a shared package exports TypeScript source files directly instead of emitting JavaScript to a dist folder. Consider a structure where packages/shared contains only .ts files and exports them via package.json exports field. A consuming package writes import { util } from "@scope/shared/util". TypeScript type-checks successfully because paths maps @scope/shared/* to ../shared/*. The bundler, however, follows exports to shared/util.ts. Without --allowImportingTsExtensions, the import specifier must omit .ts, creating an ambiguity: does util resolve to util.ts, util/index.ts, or util.js?
flowchart TD
A("Workspace import: @scope/shared/util") --> B("TypeScript paths resolve to ../shared/util")
B --> C("Bundler follows exports field")
C --> D("Exports points to shared/util.ts")
D --> E("Import lacks extension, resolution fails")
style E stroke:#fbbf24,fill:#3a2f0b,color:#fef3c7
The traditional solution splits into two camps. Teams either add a build step to packages/shared that emits JavaScript, then point exports to the dist folder, or they configure the bundler with a custom resolver plugin that maps extensionless imports to .ts files. Both approaches introduce latency: the first requires pre-building every shared package before dependent packages can bundle, and the second couples the build configuration to a specific bundler's plugin API.
--allowImportingTsExtensions collapses that complexity. When enabled, the consuming package writes import { util } from "@scope/shared/util.ts". TypeScript permits the explicit extension. The bundler receives an unambiguous path. No intermediate build step runs, and no resolver plugin is required. The tradeoff is that the flag locks the workflow into bundler-based execution because Node.js native module resolution does not handle .ts extensions without a custom loader.
Enabling --allowImportingTsExtensions in Your tsconfig.json
The flag activates in compilerOptions with one dependency: moduleResolution must be set to bundler or nodenext. The compiler enforces this constraint because the flag targets workflows where a bundler or modern loader handles TypeScript files directly. Attempting to enable it with moduleResolution: "node" triggers configuration error TS5110.
{
"compilerOptions": {
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"noEmit": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"]
}The noEmit: true setting frequently pairs with --allowImportingTsExtensions because the flag does not rewrite import specifiers during emit. If TypeScript emits JavaScript and the output contains import "./utils.ts", Node.js execution fails unless a runtime loader translates .ts to .js. Teams using the flag in monorepos typically delegate output generation entirely to bundlers (Vite, esbuild, Turbopack), making noEmit a natural fit.
Two edge cases require attention. First, if the project uses "module": "esnext" or "module": "nodenext", the compiler enforces that all import specifiers must be runtime-valid. The flag permits .ts extensions at type-check time, but the bundler must handle them at execution time or the build fails silently. Second, declaration emit (.d.ts generation) strips .ts extensions from imports. A source file containing import { T } from "./types.ts" emits import { T } from "./types" in the declaration file. This behavior prevents declaration consumers from inheriting the bundler dependency.
--allowImportingTsExtensions vs --rewriteRelativeImportExtensions: When to Use Each
TypeScript 5.7 introduced --rewriteRelativeImportExtensions as a sibling flag that serves a different use case. Where --allowImportingTsExtensions permits .ts imports without modification, --rewriteRelativeImportExtensions rewrites .ts to .js during emit. The distinction determines which workflows each flag supports.
flowchart LR
subgraph allowImportingTsExtensions["allowImportingTsExtensions workflow"]
A("Source: import from './util.ts'") --> B("Type-check passes")
B --> C("Emit (if enabled): import from './util.ts'")
C --> D("Bundler resolves .ts path")
end
subgraph rewriteRelativeImportExtensions["rewriteRelativeImportExtensions workflow"]
E("Source: import from './util.ts'") --> F("Type-check passes")
F --> G("Emit: import from './util.js'")
G --> H("Node.js resolves .js path")
end
style D stroke:#34d399,fill:#0b3b2e,color:#d1fae5
style H stroke:#34d399,fill:#0b3b2e,color:#d1fae5
--allowImportingTsExtensions requires noEmit: true or a bundler-driven workflow where TypeScript never writes JavaScript to disk. The source files contain .ts imports, the bundler receives those imports unchanged, and the bundler's native TypeScript resolver handles them. This pattern fits Vite, esbuild, and Turbopack setups where tsc runs only for type-checking (tsc --noEmit) and the bundler performs actual compilation.
--rewriteRelativeImportExtensions supports hybrid workflows where TypeScript emits JavaScript that Node.js executes directly. The source files write .ts imports for author convenience, but the emitted JavaScript contains .js extensions. This pattern fits Node.js ESM projects that require explicit extensions per the ECMAScript specification but want to avoid writing .js in TypeScript source code. The flag does not support absolute imports or package specifiers, only relative paths like ./util.ts or ../shared/api.ts.
The choice hinges on execution model. If the build pipeline runs tsc to emit JavaScript, then deploys that output to Node.js without a bundler, use --rewriteRelativeImportExtensions. If a bundler processes TypeScript source files directly and tsc runs only for type-checking, use --allowImportingTsExtensions. Mixing both flags triggers a configuration error because their rewrite behaviors conflict.
One subtle interaction: --allowImportingTsExtensions does not prevent importing .js files from TypeScript. A monorepo package can import both ./util.ts and ./legacy.js in the same source file. The flag relaxes the extension prohibition for TypeScript files; it does not restrict JavaScript imports. This matters for incremental migrations where a workspace contains a mix of TypeScript and plain JavaScript packages.
Real-World Monorepo Setup: Shared Packages With .ts Imports
A production monorepo typically structures shared code into small, focused packages under a packages/ directory. Each package exports utilities, types, or components that other packages consume. The coordination problem appears when a consuming package imports from a shared package that has not yet been built.
Consider a monorepo with three packages: @app/ui, @app/api, and @app/shared. The shared package contains utility functions and type definitions. Both ui and api depend on shared. Without --allowImportingTsExtensions, the workflow requires a build order: build shared first, emit JavaScript to shared/dist, then build ui and api. If a developer edits a file in shared, they must rebuild it before changes appear in ui or api.
flowchart LR
A("Developer edits @app/shared/utils.ts") --> B("Type-check with allowImportingTsExtensions")
B --> C("@app/ui imports from '@app/shared/utils.ts'")
C --> D("Bundler resolves .ts path directly")
D --> E("ui builds without shared pre-build step")
style D stroke:#c084fc,fill:#3b0764,color:#f3e8ff,stroke-width:4px
style E stroke:#34d399,fill:#0b3b2e,color:#d1fae5
Enabling --allowImportingTsExtensions in each package's tsconfig.json eliminates the intermediate build. The consuming package writes import { log } from "@app/shared/utils.ts". TypeScript permits the import. The bundler (typically Vite or esbuild at the monorepo root) resolves @app/shared through workspace protocol, follows the exports field to shared/src/utils.ts, and compiles it inline. The shared package never emits a dist folder. Changes propagate instantly because the bundler processes source files on every build.
The package.json configuration in each package must align. The shared package exports TypeScript source files:
{
"name": "@app/shared",
"version": "1.0.0",
"type": "module",
"exports": {
"./utils.ts": "./src/utils.ts",
"./types.ts": "./src/types.ts"
},
"files": ["src"]
}The consuming package (ui or api) declares the dependency using workspace protocol:
{
"name": "@app/ui",
"version": "1.0.0",
"dependencies": {
"@app/shared": "workspace:*"
}
}Two failure modes surface with this setup. First, if the exports field omits the .ts extension (writing "./utils": "./src/utils.ts" instead of "./utils.ts": ...), the import path and the export path mismatch. The bundler receives import from "@app/shared/utils.ts" but the exports field declares ./utils, causing a resolution failure. Second, if the consuming package imports without the extension (import from "@app/shared/utils"), TypeScript permits it but the bundler fails at runtime because the exports field specifies an exact path match.
The fix for both is consistency: always include .ts in both the import specifier and the exports field key. This rigidity is the cost of eliminating the pre-build step. The payoff is a zero-latency development loop where edits in shared appear in ui on the next bundler rebuild, typically under 100 milliseconds with Vite or esbuild.
Bundler Compatibility: How Vite, esbuild, and Turbopack Handle .ts Extensions
The viability of --allowImportingTsExtensions depends entirely on bundler support for resolving .ts paths. As of 2026, three bundlers dominate monorepo setups: Vite, esbuild, and Turbopack. Each handles TypeScript imports differently.
flowchart LR
A("Source: import from './util.ts'") --> B("Bundler receives .ts path")
B --> C("Vite resolves .ts natively")
B --> D("esbuild resolves .ts natively")
B --> E("Turbopack resolves .ts natively")
C --> F("Compilation succeeds")
D --> F
E --> F
style F stroke:#34d399,fill:#0b3b2e,color:#d1fae5
Vite 5.x and later resolve .ts extensions without configuration. The internal resolver checks for .ts, .tsx, .js, and .jsx in that order when a file path is encountered. An import like ./utils.ts resolves to utils.ts if the file exists, or falls back to utils/index.ts. The behavior aligns with Node.js ESM resolution rules but adds TypeScript extensions to the search list. This makes Vite the most ergonomic choice for monorepos using --allowImportingTsExtensions because no additional plugins or configuration are required.
esbuild handles .ts imports natively starting from version 0.20. The resolver treats .ts as a valid extension and compiles it inline. One edge case: if both utils.ts and utils.js exist in the same directory, esbuild prioritizes .ts. This differs from Node.js, which prioritizes .js. Teams migrating from a dual-source setup (TypeScript and JavaScript coexisting) must ensure no collisions exist or the wrong file resolves. The failure mode is subtle: type-checking passes because TypeScript sees utils.ts, but the runtime uses utils.js because esbuild prioritized it.
Turbopack, Vercel's Rust-based bundler, resolves .ts imports as part of its default TypeScript loader. The resolver follows the same priority rules as esbuild: .ts before .js. Turbopack's incremental compilation model benefits from explicit extensions because it caches file metadata by path. An import without an extension forces the resolver to stat multiple possible files on every build. With .ts explicit, the resolver checks one path and skips the fallback logic, reducing invalidation overhead in large monorepos.
Two bundlers do NOT support .ts imports natively as of 2026: webpack 5 and Rollup 4. Webpack requires either ts-loader or babel-loader configured to strip extensions, or a custom resolver plugin. Rollup requires @rollup/plugin-typescript with resolveExtensions configured to include .ts. Both introduce plugin maintenance and coupling. Teams standardizing on --allowImportingTsExtensions should default to Vite, esbuild, or Turbopack to avoid this friction.
One compatibility hazard spans all bundlers: circular imports with .ts extensions. When two files import each other using explicit .ts paths, the bundler must detect the cycle and hoist declarations. esbuild and Turbopack handle this correctly. Vite 5.0 through 5.2 had a bug where circular .ts imports caused module initialization failures at runtime. Vite 5.3 patched the issue. The lesson: test circular imports explicitly when enabling the flag in a monorepo that reuses types bidirectionally across packages.
Migration Strategy: Moving an Existing Monorepo to --allowImportingTsExtensions
Migrating a production monorepo to --allowImportingTsExtensions requires three phases: validation, rewrite, and verification. The critical constraint is maintaining type-checking correctness throughout the migration because a large monorepo cannot be migrated atomically without downtime.
flowchart LR
A("Audit import specifiers") --> B("Enable flag in one package")
B --> C("Rewrite imports to include .ts")
C --> D("Run type-check and build")
D --> E("Expand to dependent packages")
E --> F("Remove path-rewrite plugins")
style C stroke:#c084fc,fill:#3b0764,color:#f3e8ff,stroke-width:4px
style F stroke:#34d399,fill:#0b3b2e,color:#d1fae5
Phase one audits existing import specifiers. Run a codebase search for import.*from ['"]\.\.?\/.*['"]\s*;? to locate all relative imports. Filter results to identify which imports resolve to TypeScript files versus JavaScript files. The distinction matters because the flag applies only to .ts, .tsx, .mts, and .cts imports. JavaScript imports remain unchanged. A monorepo with mixed TypeScript and JavaScript packages will have both categories.
Phase two enables the flag incrementally. Select a leaf package (one with no dependents) and add "allowImportingTsExtensions": true to its tsconfig.json. Rewrite its imports to include .ts extensions. Use a tool like ts-migrate or a custom codemod script to automate the rewrite. The script must distinguish between relative imports (./utils) that resolve to TypeScript files and those that resolve to JavaScript files. Only the former need .ts appended.
Run tsc --noEmit after the rewrite to verify type-checking still passes. Then run the bundler (vite build or esbuild) to verify the output compiles. The failure mode here is import path mismatches: if an import was rewritten to ./utils.ts but the file is actually named utils.tsx, the build fails. The fix is correcting the extension in the import specifier, not reverting the flag.
Phase three expands the flag to dependent packages. Once the leaf package builds successfully, enable the flag in packages that depend on it. Rewrite their imports to include .ts when importing from the migrated package. This cascades upward through the dependency graph until all packages in the monorepo have the flag enabled. The migration is complete when no package uses path-rewrite plugins or custom resolver configuration.
Two rollback strategies exist. If a package fails to build after enabling the flag, the immediate fix is disabling the flag and reverting import rewrites for that package only. The rest of the monorepo continues using the flag. This partial rollback works because --allowImportingTsExtensions is package-scoped; enabling it in one tsconfig.json does not force dependent packages to adopt it. The second rollback is full reversion: disable the flag across the monorepo and restore the original import specifiers. This is viable if the migration uncovers a bundler compatibility issue that blocks production builds.
One migration hazard: type-only imports. TypeScript strips type-only imports during compilation. An import like import type { T } from "./types.ts" compiles to nothing in the JavaScript output. If the bundler's TypeScript loader does not recognize import type syntax, it attempts to load types.ts at runtime, causing a module-not-found error. Vite, esbuild, and Turbopack all handle import type correctly, but custom loaders or older bundlers may not. The fix is ensuring the bundler's TypeScript configuration includes "importsNotUsedAsValues": "remove" or "verbatimModuleSyntax": true.
When NOT to Use --allowImportingTsExtensions in 2026
The flag solves bundler-driven workflows but introduces new constraints that make it unsuitable for certain setups. Three scenarios should avoid enabling it: Node.js native execution, library publishing, and polyglot monorepos mixing TypeScript and non-TypeScript build tools.
Node.js 22.x does not resolve .ts extensions without a custom loader. An import like import { fn } from "./utils.ts" fails with ERR_UNKNOWN_FILE_EXTENSION. The workaround is running Node.js with --experimental-loader pointing to a TypeScript loader like tsx or ts-node/esm. This introduces runtime overhead and couples the deployment to a specific loader implementation. Teams deploying to serverless environments (AWS Lambda, Cloudflare Workers) cannot rely on custom loaders because the platform controls the Node.js invocation. For these cases, --rewriteRelativeImportExtensions is the correct choice because it emits .js imports that Node.js resolves natively.
Library publishing presents a second constraint. If a package in the monorepo is published to npm for external consumption, the declaration files (.d.ts) must not contain .ts extensions in import specifiers. TypeScript strips .ts from declarations when --allowImportingTsExtensions is enabled, but the consuming project's bundler receives those extensionless imports. If the consumer has not enabled --allowImportingTsExtensions, their type-checking fails. The safe pattern for published libraries is omitting extensions in source code and relying on the bundler's default resolution.
Polyglot monorepos that mix TypeScript with Rust, Go, or other compiled languages introduce a third incompatibility. If a TypeScript package imports from a Rust-compiled WebAssembly module or a Go-compiled binary, the import specifier must match the emitted file extension (.wasm, .node). TypeScript does not permit .wasm imports without --allowArbitraryExtensions, which conflicts with --allowImportingTsExtensions. The result is a configuration impasse. The workaround is isolating TypeScript packages into a subdirectory with their own tsconfig.json that enables the flag, while the polyglot packages use a separate configuration.
That covers the essential patterns for --allowImportingTsExtensions in monorepos. Enable it when the build pipeline uses Vite, esbuild, or Turbopack for bundling and tsc runs only for type-checking. Avoid it when targeting Node.js native execution, publishing libraries to npm, or managing polyglot codebases. Apply these rules in a production monorepo and the difference will be immediate: faster development loops, fewer build-order dependencies, and explicit import paths that eliminate resolution ambiguity.
Frequently Asked Questions
Does --allowImportingTsExtensions work with path mappings in tsconfig.json?
Yes. The flag permits .ts extensions in import specifiers but does not affect path mapping resolution. If paths maps @lib/* to ../lib/src/*, an import like import { fn } from "@lib/utils.ts" resolves through the mapping first, then checks for utils.ts in the mapped directory. The bundler must support the same path mapping via its resolver configuration (Vite's resolve.alias, esbuild's alias plugin).
Can a monorepo enable --allowImportingTsExtensions in some packages but not others?
Yes. Each package's tsconfig.json controls the flag independently. A leaf package can enable it while a dependent package omits it. The dependent package writes imports without .ts extensions, and the leaf package's exports must accommodate both forms. This typically requires dual exports entries in the leaf package's package.json: one with .ts and one without. The pattern complicates maintenance and should only be used during incremental migration.
What happens if a TypeScript file and a JavaScript file have the same name in the same directory?
Bundler behavior varies. Vite and Turbopack prioritize .ts over .js when both exist. esbuild prior to 0.21 prioritized .js. The safest pattern is never colocating .ts and .js files with identical names. During migration, rename one file or move it to a subdirectory to avoid resolution ambiguity.
Does --allowImportingTsExtensions affect JSON or CSS imports?
No. The flag applies only to TypeScript file extensions (.ts, .tsx, .mts, .cts). JSON imports (import data from "./config.json") and CSS imports (import "./styles.css") follow separate resolution rules controlled by the bundler's loader configuration. Those imports remain unchanged when enabling the flag.
Why does TypeScript strip .ts extensions from declaration files?
Declaration files are consumed by other TypeScript projects that may not have --allowImportingTsExtensions enabled. If a declaration contained import { T } from "./types.ts", the consuming project would fail to type-check unless it also enabled the flag. Stripping .ts ensures declarations remain compatible with all TypeScript configurations. The bundler resolves the actual source file; the declaration only provides type information.