QA How-To
How to Fix Cypress "Unknown file extension .ts"
Fix Cypress unknown file extension ts errors by identifying the failing loader, aligning ESM config, and verifying TypeScript specs in local runs and CI.
17 min read | 2,955 words
TL;DR
Run the project-local Cypress CLI with the intended TypeScript config and a small .cy.ts spec. If the stack trace starts in a standalone Node script, run that script with a TypeScript-capable runner; if only CI fails, compare installed Cypress, Node, and working directory.
Key Takeaways
- Use the path and first stack frame to find the process that tried to load .ts.
- Run TypeScript config and specs through the local Cypress CLI.
- Match config extension, package module type, and TypeScript module settings.
- Give standalone Node wrappers their own TypeScript runner when needed.
- Compare local, CI, and Docker versions before changing application code.
- Verify with a no-server .cy.ts smoke spec, then rerun the failing spec.
To fix Cypress unknown file extension ts errors, first check when the message appears: Cypress startup, a Node wrapper before Cypress starts, or a CI-only run. The same .ts suffix can reach different loaders, so the command that fails matters more than the filename alone.
TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" for /project/cypress.config.ts
The path after for will be your own path. Keep that path and the first stack frame in the log; together they identify the file and process to repair. The examples below use a small end-to-end spec that needs no running application.
TL;DR
Run the config through the Cypress CLI, use the project's locally installed Cypress, and confirm the CLI points at the intended config. For a current Cypress installation, cypress.config.ts is supported directly. A plain node cypress.config.ts or a separate Node script importing that config is a different execution path and may need a TypeScript runner.
npm ci
npx cypress version
npx cypress run --e2e --config-file cypress.config.ts --spec cypress/e2e/smoke.cy.ts
Create the minimal config and spec in section 1 before running the last command. If the error comes from node scripts/run-tests.mjs, run or compile that wrapper as described in section 4. Do not install a random loader into the Cypress browser bundle to solve a Node startup error.
What the Error Actually Means
ERR_UNKNOWN_FILE_EXTENSION is a Node module-loading error. Node has reached a .ts path but the loader active in that process cannot interpret it. Cypress normally processes its own TypeScript config and bundles TypeScript specs. That support does not automatically extend to every Node process invoked by an npm script, CI job, pretest hook, or custom task. The Cypress TypeScript documentation describes its config loading and supported .ts, .mts, and .cts extensions.
Read the path in the error. cypress.config.ts suggests config startup or a script importing the config. scripts/run-tests.ts points to a direct Node entry point. cypress/e2e/login.cy.ts can mean a custom preprocessor or an external tool is importing a spec outside Cypress's normal spec pipeline. A file under node_modules calls for checking the dependency's published entry point, not renaming your test.
Use these commands from the directory with the project package.json:
node --version
npm ls cypress typescript tsx --depth=0
npx cypress version
Record the exact command that fails and whether npx cypress version reports the expected local package. Do not infer a TypeScript compilation problem from editor diagnostics alone. tsc checks types; the runtime error concerns who loads the file. The Node TypeScript documentation also explains that Node's native TypeScript behavior depends on its release and does not read tsconfig.json to transform path aliases.
Root-Cause Decision Table
| Symptom | Root cause | Fix |
|---|---|---|
node cypress.config.ts fails before Cypress opens |
Node was asked to execute a Cypress config directly | Invoke npx cypress run and let Cypress load its config |
| Local CLI succeeds but CI fails at config startup | Different installed Cypress, Node, or working directory | Restore the lockfile install and print versions in CI |
.ts config fails when a JavaScript config succeeds |
Config format and package module boundary are inconsistent, or a separate loader imports it | Align config extension with package type and identify the actual importer |
Stack trace begins in scripts/run-tests.mjs |
Wrapper imports a TypeScript module without a suitable loader | Use tsx for that wrapper or compile it first |
Only one spec fails after a custom file:preprocessor hook |
Hook hands raw .ts to Node |
Restore the default pipeline or register a TypeScript-capable preprocessor |
| Docker fails while the host passes | Image, mounted files, or installed dependencies differ | Match the project's Cypress release and run from the project root |
1. Fix Cypress Unknown File Extension TS by Using the Cypress Loader
Start with the shortest possible supported path. Cypress's config is an executable Node-side module, but Cypress supplies the machinery to load its TypeScript file. Running the file with node bypasses that machinery. A direct node cypress.config.ts failure therefore does not prove that Cypress itself cannot load the config.
Put this config at the project root. It uses a real defineConfig export and only selects a TypeScript end-to-end spec pattern:
// cypress.config.ts
import { defineConfig } from 'cypress'
export default defineConfig({
e2e: {
specPattern: 'cypress/e2e/**/*.cy.ts',
},
})
Add one spec at cypress/e2e/smoke.cy.ts. It tests a value inside the Cypress command queue, so it needs neither a web server nor credentials:
// cypress/e2e/smoke.cy.ts
describe('TypeScript loading', () => {
it('runs a TypeScript spec', () => {
const answer: number = 42
cy.wrap(answer).should('eq', 42)
})
})
Verify through the local CLI:
npx cypress run --e2e --config-file cypress.config.ts --spec cypress/e2e/smoke.cy.ts
A passing test establishes that the config and spec both crossed their intended loaders. If Cypress reports no specs, inspect specPattern and the path passed to --spec; the CLI only runs files that match both. If the error persists before the browser opens, keep the first stack frame and continue to section 2. The Cypress CLI reference documents --config-file, --spec, and --e2e.
Do not treat npx tsc --noEmit as the sole verification. It can pass while the wrong process still imports .ts at runtime. Conversely, an editor's TypeScript warning may coexist with a working Cypress run. Diagnose those separately.
2. Fix Cypress Unknown File Extension TS After an Install
The npx command may use a different package than you expect if the project has no local installation or if a CI step installs from a different lockfile. Compare the root package.json, lockfile, and installed tree. Cypress documents which TypeScript releases its current package supports; match that published compatibility instead of guessing a pin.
npm ls cypress typescript --depth=0
npx cypress version
npm pkg get devDependencies.cypress devDependencies.typescript
If npm ls shows a missing dependency, install it in the actual project and commit the resulting manifest and lockfile as part of your own project change. For a new example project, the commands are:
npm install --save-dev cypress typescript
npx cypress verify
npx cypress run --e2e --config-file cypress.config.ts --spec cypress/e2e/smoke.cy.ts
cypress verify checks the installed Cypress binary. It does not execute the spec, which is why the final command is still required. On an existing team project, first use npm ci against its checked-in lockfile; do not silently replace its dependency policy with the latest release. If the installed release does not support the config format you selected, update Cypress under that policy or use a compatible JavaScript config until the upgrade is scheduled.
Keep dependency errors distinct. Cannot find package 'cypress' and a failed binary verification are installation problems; neither is the same Node extension error. Likewise, missing typescript can affect type checking or supported config processing, but the exact stack trace should guide the repair.
3. Align the Config Extension With the Module Boundary
A project's nearest package.json determines whether a plain .ts config is treated as an ES module or CommonJS by current Cypress config loading rules. .mts forces ESM and .cts forces CommonJS. TypeScript's compilerOptions.module should agree with the chosen runtime format, but setting it alone does not change which module loader Cypress selects. The Cypress configuration reference documents the config export shape and module behavior.
For a package that declares ESM, this is a coherent minimal setup. The JSON is an excerpt from package.json, and the config is the same file introduced in section 1:
{
"type": "module",
"devDependencies": {
"cypress": "<match-your-lockfile>",
"typescript": "<match-your-lockfile>"
}
}
The placeholder values indicate the installed versions; do not paste them as literal package versions. If your package has no type field and you need an unambiguous ESM config, rename the config to cypress.config.mts and pass that exact filename to --config-file. Keep import { defineConfig } from 'cypress' and export default defineConfig(...) in the renamed file.
npx cypress run --e2e --config-file cypress.config.mts --spec cypress/e2e/smoke.cy.ts
For an intentionally CommonJS configuration, use cypress.config.cts and a CommonJS-compatible TypeScript setup. Avoid flipping the root package's type field merely to quiet one test runner; that change affects every .js file in the package. If a migration produces require is not defined in ES module scope or module is not defined in ES module scope, those are module-format errors. Fix the import/export style or extension before chasing .ts handling again.
Check which file Cypress actually selected. A stale --config-file tests/cypress.config.js in an npm script can hide your corrected root config. Run npm pkg get scripts and compare the printed command with the path in the failure. A monorepo package should run Cypress from its package directory or set --project deliberately; an accidental repository-root run can resolve a different config.
4. Give an External Node Wrapper Its Own TypeScript Runner
Many failures attributed to Cypress happen one process earlier. Consider a wrapper that imports a TypeScript helper, prepares test data, then starts Cypress. Cypress has no opportunity to process the helper because the wrapper is executed by Node. Current Node releases may support some native TypeScript, but older or differently configured CI runtimes may not, and native type stripping has limits. Use a dedicated runner when the wrapper needs consistent behavior across environments.
Create a helper and a wrapper with matching imports. This example uses tsx only for the external script; Cypress still loads cypress.config.ts itself:
// scripts/run-tests.ts
import { spawnSync } from 'node:child_process'
const result = spawnSync(
process.platform === 'win32' ? 'npx.cmd' : 'npx',
['cypress', 'run', '--e2e', '--config-file', 'cypress.config.ts', '--spec', 'cypress/e2e/smoke.cy.ts'],
{ stdio: 'inherit' },
)
if (result.error) throw result.error
process.exit(result.status ?? 1)
Install tsx as a development dependency in that package, then run the wrapper through its documented CLI:
npm install --save-dev tsx
npx tsx scripts/run-tests.ts
A pass verifies both the wrapper and Cypress invocation. node scripts/run-tests.ts is a different experiment and may depend on Node's native TypeScript support. For an npm script, use a command such as tsx scripts/run-tests.ts, then verify with npm run e2e after defining that script. node --import tsx scripts/run-tests.ts is another supported execution form documented by tsx, but adding a loader globally through NODE_OPTIONS affects every child Node process and can complicate diagnosis.
If the wrapper only shells out to Cypress, a direct npm script with cypress run is simpler. Retain a wrapper when it performs a real preparatory step, and keep its process exit status so CI fails when Cypress fails. Never import cypress/e2e/smoke.cy.ts directly from the wrapper: the spec expects Cypress globals and its browser-side compilation pipeline.
5. Remove a Preprocessor That Sends Raw TypeScript to Node
Cypress normally compiles TypeScript specs. A custom file:preprocessor hook can replace that default path. If a plugin was copied from an old JavaScript-only setup, it may call Node require() or dynamic import() on the raw .cy.ts path. The extension error then originates in the plugin, not in Cypress's built-in TypeScript support.
Inspect the config's setupNodeEvents for on('file:preprocessor', ...). Temporarily test with the minimal config from section 1. If that run passes, restore the rest of your configuration one part at a time until the hook is isolated. For projects that do not need a custom bundler, remove the hook and use the default spec pipeline:
// cypress.config.ts
import { defineConfig } from 'cypress'
export default defineConfig({
e2e: {
specPattern: 'cypress/e2e/**/*.cy.ts',
},
})
Verify the exact spec again:
npx cypress run --e2e --config-file cypress.config.ts --spec cypress/e2e/smoke.cy.ts
If your suite relies on a custom bundler for aliases or framework transforms, configure that preprocessor to compile .ts and .tsx according to its own documentation. Do not add a guessed require.extensions['.ts'] hook. Such a hook changes process-wide behavior and does not solve ESM imports. The Cypress component testing guide is useful when the failing file is a component spec, because component specs depend on the selected framework dev server rather than the E2E preprocessor alone.
A missing cy type in the editor is also not this runtime error. Put Cypress-specific types in cypress/tsconfig.json and keep Jest or Vitest globals separate if the project uses both. Confirm type resolution with npx tsc --project cypress/tsconfig.json --noEmit, then confirm runtime loading with the Cypress CLI. Each check answers a different question.
6. Reproduce the Failure in CI Before Changing Code
CI often reveals a mismatch hidden by a developer's global tools or cached modules. Log the versions and working directory before the test command. Use the project's package lock to install dependencies, then run the same spec as on the workstation. This GitHub Actions job is a template; choose a Node release supported by your installed Cypress package and your own application:
name: Cypress TypeScript smoke
on: [push, pull_request]
jobs:
smoke:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm
- run: npm ci
- run: node --version
- run: npx cypress version
- run: npx cypress run --e2e --config-file cypress.config.ts --spec cypress/e2e/smoke.cy.ts
The action tags are published action major releases, not invented Cypress or Node pins. The job assumes your repository already has .nvmrc; if it uses a different version file, update node-version-file to match. The verification is the final run, while the preceding lines show whether CI installed the same toolchain. On a monorepo, set working-directory on the install and run steps to the package containing Cypress, or use --project with a verified path.
Docker has another version boundary. cypress/included images contain a particular global Cypress release; your repository may install a different local one. Select a published image tag that matches your lockfile and desired Node/browser combination, and compare the versions inside the container. The placeholder below must be replaced with an actual published tag:
docker run --rm --entrypoint sh -v "$PWD:/e2e" -w /e2e cypress/included:<matching-published-tag> -lc 'npm ci && npx cypress version && npx cypress run --e2e --config-file cypress.config.ts --spec cypress/e2e/smoke.cy.ts'
If bind-mounted node_modules comes from a different operating system, remove that mount or install inside the container. If the image's entrypoint changes how commands are invoked, use the image's documented invocation and retain the same npm ci, version check, and run sequence. The Cypress CI documentation describes the image families and their contents.
How to Verify the Fix
Verify the smallest path first, then restore the actual suite. Run the local smoke spec, your real failing spec, and the original CI command in that order. Keep the command and environment identical between workstation and CI when comparing results:
npx cypress run --e2e --config-file cypress.config.ts --spec cypress/e2e/smoke.cy.ts
npx cypress run --e2e --config-file cypress.config.ts --spec cypress/e2e/your-failing-spec.cy.ts
The second path is a placeholder for a real existing spec. A successful run means Cypress loaded the config, selected the spec, executed its browser code, and exited successfully. A No spec files were found message means discovery still needs work; it is not proof that TypeScript loading was repaired. A passing cypress verify alone proves only that the binary can start.
If the minimal spec passes but a production spec does not, inspect imports in that spec and its support file. A Node-only package imported into browser code can fail during bundling; an unresolved alias can fail during module resolution. Those failures need their own stack trace. Use the Cypress debugging guide to isolate the first failing import rather than editing the config repeatedly.
For a precise handoff, record the failing command, Node and Cypress versions, the config filename, the first stack frame, and whether the smoke spec passed. That evidence lets another engineer reproduce the loader boundary without access to your full CI log.
Prevent It From Coming Back
Keep one documented command for local and CI runs, preferably an npm script that invokes the local Cypress binary. Commit the lockfile and use npm ci in CI. Avoid relying on a globally installed cypress executable, since its release may drift from the repository's dependency. Pin Node through the repository's version-management file and make CI read the same file.
Review config paths when moving tests in a monorepo. The extension and package.json module type are part of the config's runtime contract, while tsconfig.json is the type-checking contract. Add a smoke spec that has no application dependency so a loader regression is distinguishable from an unavailable server. Keep that smoke command early in CI if startup failures have been costly.
Use the Cypress test architecture guide to decide where Node-side setup belongs, and the Node.js guide for testers when scripts are maintained by the QA team. For spec discovery issues, the single-spec Cypress guide covers the relationship between --spec and configured patterns. Those boundaries are more durable than a one-off loader flag.
Interview Questions and Answers
Q: Why can cypress.config.ts work in Cypress but fail with node cypress.config.ts?
Cypress controls how it loads its config and can transpile TypeScript during startup. A direct Node command uses Node's own support and flags, which vary by release. I would test the Cypress CLI before modifying the config.
Q: What evidence distinguishes an unsupported extension from a missing spec?
An extension failure contains ERR_UNKNOWN_FILE_EXTENSION and names the file Node tried to load. A missing spec is reported by Cypress after discovery and points to specPattern or --spec. I would preserve both the command and the first stack frame.
Q: Does changing compilerOptions.module alone choose the config loader?
No. The config extension and nearest package.json module type determine the runtime module format in current Cypress loading. The TypeScript setting should align with that format so type checking reflects execution.
Q: How would you investigate a CI-only failure?
I would print node --version, npx cypress version, and the working directory, then confirm npm ci used the committed lockfile. I would run a no-server smoke spec before the application suite. That separates loader startup from application availability.
Q: When is tsx appropriate here?
It is appropriate when a separate Node script needs to execute TypeScript consistently. It is unnecessary for a normal Cypress spec or config on a supported Cypress installation. I would scope it to the wrapper's command.
Q: What does a custom preprocessor change?
It can replace Cypress's default spec bundling path. If it imports raw .ts into Node, the error may originate there. I would remove the hook temporarily, prove the default path works, then configure a TypeScript-capable preprocessor only if the custom transform is required.
Common Mistakes
- Running
node cypress.config.tsas a validation command and treating its result as a Cypress test. - Installing a loader globally without identifying the process that loads the
.tsfile. - Changing the root package's
typefield without checking every JavaScript script in that package. - Pointing
--config-fileat an old JavaScript file while editing a different TypeScript config. - Treating a
tscpass, acypress verifypass, or a no-spec run as an executed test. - Using a Docker image whose Cypress release differs from the project lockfile without noticing the mismatch.
- Renaming a
.cy.tsspec to.jsto conceal an incompatible custom preprocessor.
Conclusion
To fix Cypress "Unknown file extension .ts", identify which process loaded the named file and put that file through the correct loader. The normal route is the local Cypress CLI for config and specs; an external Node wrapper needs its own TypeScript execution plan. Prove the repair with a no-server TypeScript smoke spec, then rerun the original failing command in the same environment.
Interview Questions and Answers
How do you locate the source of ERR_UNKNOWN_FILE_EXTENSION in a Cypress project?
I capture the failing command, named .ts path, and first stack frame. I compare that path with the Cypress config, spec, support files, and any external wrapper. Then I run a minimal TypeScript spec through the local CLI to separate Cypress loading from a custom process.
Why is node cypress.config.ts a poor Cypress validation command?
It invokes Node directly and bypasses Cypress config processing. The result measures the Node runtime and its flags rather than the test runner. I validate through npx cypress run with an explicit --config-file path.
What is the relationship between .ts, .mts, .cts, and package type?
A plain .ts file follows its package module boundary in current Cypress config loading. .mts selects ESM and .cts selects CommonJS explicitly. I align the TypeScript module setting with the chosen format so editor checks and runtime interpretation agree.
How do you distinguish a missing TypeScript spec from a loader failure?
A missing spec appears after Cypress applies specPattern and --spec. A loader failure carries ERR_UNKNOWN_FILE_EXTENSION and names a file Node could not load. I do not treat a no-spec exit as a successful TypeScript run.
What is your CI triage sequence for a local-pass, CI-fail config?
I compare Node and Cypress versions, current directory, dependency installation, and the selected config path. I install from the lockfile and run a no-server smoke spec. Then I compare the first failing stack frame against the local run.
When would you add tsx to a Cypress repository?
I add it when an independent Node script executes TypeScript, such as a test orchestration wrapper. Cypress already owns its config and spec pipelines on supported releases. I keep the tsx invocation scoped to the external script and verify its exit code propagates.
How can a file preprocessor create this error?
A custom file:preprocessor handler can bypass default TypeScript bundling and pass a raw .ts file to Node. I temporarily restore the default pipeline and rerun one spec. If it passes, I fix the custom transform or remove the hook.
Frequently Asked Questions
Why does Cypress say Unknown file extension .ts?
A Node process reached a TypeScript file without a loader that can execute it. Check the path in the error and the command that launched the process. The cause differs between Cypress config startup and a separate Node wrapper.
Can Cypress run cypress.config.ts without ts-node?
Current Cypress releases support TypeScript configuration files directly through their own config loader. Use the local Cypress CLI and a release compatible with your installed TypeScript. Do not use a direct Node command as the test of Cypress support.
Should I rename cypress.config.ts to .js?
Only if your project intentionally chooses a JavaScript config. Renaming can mask an outdated runner, wrong config path, or external importer. First prove whether the local Cypress CLI can load a minimal TypeScript config.
Does type module cause the .ts extension error?
The package module boundary affects how a config is interpreted, and a mismatched loader can expose a .ts extension error. Check the nearest package.json and use .mts or .cts when you need an explicit module format. Do not flip the package type without reviewing other scripts.
Why does this happen only in GitHub Actions?
CI can install a different dependency tree, use a different Node release, or run from a different directory. Print versions, install from the committed lockfile, and execute the same minimal spec locally and in CI. The first stack frame shows whether failure occurs before Cypress starts.
Will npx cypress verify prove the TypeScript fix?
No. That command verifies the Cypress binary installation. Run a real .cy.ts spec with the TypeScript config to prove config loading, discovery, browser bundling, and test execution.
Should I add NODE_OPTIONS with a TypeScript loader?
Use a runner such as tsx only for a separate Node entry point that needs TypeScript. A process-wide NODE_OPTIONS setting affects child processes and can make failures harder to isolate. Normal Cypress config and spec loading should be tested through the Cypress CLI first.
Related Guides
- How to Fix "Cypress failed to start" and cypress verify Errors
- How to Fix Cypress "cy.visit() failed trying to load"
- How to Download a file in Cypress (2026)
- How to Fix "Playwright Test did not expect test() to be called here"
- How to Fix "The Cypress binary is missing" in CI
- How to Fix Appium WebDriverAgent Failed to Start on iOS