Keywords: Node.js | TypeScript | ERR_UNKNOWN_FILE_EXTENSION | Module System | ts-node
Abstract: This article provides an in-depth analysis of the common ERR_UNKNOWN_FILE_EXTENSION error in Node.js TypeScript projects, typically caused by incompatibility between module type configuration in package.json and ts-node. Starting from the root cause of the error, it explains the differences between CommonJS and ES module systems, offers multiple solutions including removing type:module configuration, using ts-node-esm, and configuring tsconfig.json, and demonstrates implementation details through practical code examples. The article also explores alternative tools like tsx, helping developers choose the most suitable TypeScript execution solution based on project requirements.
Error Phenomenon and Background Analysis
During the deployment of Node.js TypeScript projects, developers frequently encounter the TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" error. This error typically occurs when deploying to cloud platforms like Heroku, indicating that the Node.js runtime cannot recognize the .ts file extension. From the provided error logs, we can see that the project uses the ts-node src/App.ts command to start, but fails to properly process TypeScript files in the ES module environment.
Root Cause Investigation
The core issue lies in the incompatibility between the "type": "module" configuration in package.json and ts-node's default behavior. When type: module is set, Node.js enables the ES module loader, which by default only supports standard file extensions like .js, .mjs, .cjs, and cannot recognize .ts files. As a TypeScript execution environment, ts-node requires specific module loading mechanisms to handle TypeScript file compilation.
From a technical architecture perspective, Node.js has two main module systems: CommonJS and ES modules. CommonJS uses require() and module.exports, while ES modules use import and export syntax. ts-node was initially designed primarily for CommonJS environments, causing module system conflicts when projects enforce ES module usage.
Solution One: Remove type:module Configuration
The most straightforward solution is to remove the "type": "module" configuration from package.json. This approach is suitable for projects that don't require strict ES module features, or where module imports can be handled through other means.
Modified package.json example:
{
"name": "discordtoornamentmanager",
"version": "1.0.0",
"description": "",
"main": "dist/app.js",
"scripts": {
"test": "echo "Error: no test specified" && exit 1",
"dev": "nodemon -x ts-node src/App.ts",
"start": "ts-node src/App.ts"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@types/node": "^14.0.5",
"axios": "^0.19.2",
"discord.js": "^12.2.0",
"pg": "^8.2.1",
"reflect-metadata": "^0.1.10",
"typeorm": "0.2.25",
"typescript": "^3.9.3",
"nodemon": "^2.0.4",
"ts-node": "8.10.1"
}
}The advantage of this method is its simplicity and directness, requiring no additional configuration changes. However, the drawback is that if the project genuinely requires ES module features (such as top-level await, static import analysis, etc.), certain functionalities might be affected.
Solution Two: Configure ES Module Support
If the project must retain ES module features, compatibility can be achieved by configuring tsconfig.json and modifying startup commands. This approach requires ensuring that TypeScript configuration properly supports ES modules and using specific ts-node commands.
First, ensure ES module-related configuration in tsconfig.json:
{
"compilerOptions": {
"lib": ["es6"],
"target": "es6",
"module": "ESNext",
"moduleResolution": "node",
"outDir": "dist",
"resolveJsonModule": true,
"emitDecoratorMetadata": true,
"esModuleInterop": true,
"experimentalDecorators": true,
"sourceMap": true
},
"ts-node": {
"esm": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "**/*.spec.ts"]
}Then modify the startup scripts in package.json:
"scripts": {
"start": "ts-node --esm src/App.ts",
"dev": "nodemon -x ts-node --esm src/App.ts"
}Alternatively, use Node.js's native ES module loader:
"scripts": {
"start": "node --loader ts-node/esm src/App.ts"
}Solution Three: Using Alternative Tool tsx
For developers consistently facing ts-node compatibility issues, consider using tsx as an alternative. tsx is a TypeScript executor based on esbuild, offering better ES module compatibility and faster startup speeds.
Installing and using tsx:
npm install -D tsx
npx tsx src/App.tsOr configure scripts in package.json:
"scripts": {
"start": "tsx src/App.ts",
"dev": "nodemon -x tsx src/App.ts"
}tsx's advantage lies in its ability to handle TypeScript files in ES module environments without complex configuration, while leveraging esbuild's fast compilation capabilities to significantly improve development experience.
In-Depth Technical Principles
To thoroughly understand this issue, one must delve into Node.js's module loading mechanism. Node.js's ES module loader is enabled through the --loader flag or package.json's type field, following strict ES module specifications including file extension validation, static import analysis, and other features.
ts-node works by compiling TypeScript code to JavaScript at runtime, then executing it through Node.js. In CommonJS environments, ts-node intercepts .ts file loading by modifying require.extensions. However, in ES module environments, this mechanism is no longer applicable, requiring different technical approaches.
ts-node's ES module support is implemented in two ways: first, using the ts-node-esm command-line tool, and second, through the --loader ts-node/esm parameter. Both methods utilize Node.js's experimental loader API to extend module resolution capabilities.
Version Compatibility Considerations
From the reference articles, we can see that Node.js version upgrades often bring changes to the module system. ES module support gradually stabilized in Node.js v12+ versions, but still contained many experimental features during v14-v16. Developers need to pay attention to compatibility between their Node.js version and ts-node version.
Recommended version combinations:
- Node.js 14+ with ts-node 10+
- Ensure TypeScript configuration's target matches the Node.js version
- When deploying to platforms like Heroku, specify Node.js version through engines field
Best Practice Recommendations
Based on years of TypeScript project development experience, we recommend:
- Clarify Project Requirements: If specific ES module features are not needed, prioritize CommonJS mode
- Unify Build Configuration: Ensure consistent module configuration between development and production environments
- Gradual Migration: For existing projects, gradually test ES module compatibility
- Toolchain Selection: Choose ts-node or tsx based on project scale, consider compilation deployment for large projects
By understanding module system working principles and toolchain compatibility characteristics, developers can effectively avoid ERR_UNKNOWN_FILE_EXTENSION errors and build stable, reliable TypeScript applications.