Question
I am deploying a basic TypeScript app to Heroku using ts-node and nodemon, but the app crashes on startup with this error:
2020-05-30T00:03:12.201106+00:00 heroku[web.1]: Starting process with command `npm start`
2020-05-30T00:03:14.405285+00:00 app[web.1]:
2020-05-30T00:03:14.405303+00:00 app[web.1]: > discordtoornamentmanager@1.0.0 start /app
2020-05-30T00:03:14.405303+00:00 app[web.1]: > ts-node src/App.ts
2020-05-30T00:03:14.405304+00:00 app[web.1]:
2020-05-30T00:03:14.833655+00:00 app[web.1]: (node:23) ExperimentalWarning: The ESM module loader is experimental.
2020-05-30T00:03:14.839311+00:00 app[web.1]: TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" for /app/src/App.ts
2020-05-30T00:03:14.839312+00:00 app[web.1]: at Loader.defaultGetFormat [as _getFormat] (internal/modules/esm/get_format.js:65:15)
2020-05-30T00:03:14.839314+00:00 app[web.1]: at Loader.getFormat (internal/modules/esm/loader.js:113:42)
2020-05-30T00:03:14.839315+00:00 app[web.1]: at Loader.getModuleJob (internal/modules/esm/loader.js:244:31)
2020-05-30T00:03:14.839315+00:00 app[web.1]: at processTicksAndRejections (internal/process/task_queues.js:97:5)
2020-05-30T00:03:14.839316+00:00 app[web.1]: at Loader.import (internal/modules/esm/loader.js:178:17)
2020-05-30T00:03:14.847801+00:00 app[web.1]: npm ERR! code ELIFECYCLE
2020-05-30T00:03:14.847998+00:00 app[web.1]: npm ERR! errno 1
2020-05-30T00:03:14.848957+00:00 app[web.1]: npm ERR! discordtoornamentmanager@1.0.0 start: `ts-node src/App.ts`
2020-05-30T00:03:14.849050+00:00 app[web.1]: npm ERR! Exit status 1
2020-05-30T00:03:14.849172+00:00 app[web.1]: npm ERR!
2020-05-30T00:03:14.849254+00:00 app[web.1]: npm ERR! Failed at the discordtoornamentmanager@1.0.0 start script.
2020-05-30T00:03:14.849337+00:00 app[web.1]: npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
2020-05-30T00:03:14.854859+00:00 app[web.1]:
2020-05-30T00:03:14.854998+00:00 app[web.1]: npm ERR! A complete log of this run can be found in:
2020-05-30T00_03_14_850Z-debug.log
2020-05-30T00:03:14.907689+00:00 heroku[web.1]: Process exited with status 1
2020-05-30T00:03:14.943718+00:00 heroku[web.1]: State changed from starting to crashed
My package.json is:
{
"name": "discordtoornamentmanager",
"version": "1.0.0",
"description": "",
"main": "dist/app.js",
"type": "module",
"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"
My tsconfig.json is:
{
"compilerOptions": {
"lib": ["es6"],
"target": "es6",
"module": "commonjs",
"moduleResolution": "node",
"outDir": "dist",
"resolveJsonModule": true,
"emitDecoratorMetadata": true,
"esModuleInterop": true,
"experimentalDecorators": true,
"sourceMap": true
},
"include": ["src/**/*.ts"]
Why does Node/Heroku report Unknown file extension ".ts", and how should this TypeScript app be configured correctly?
Short Answer
By the end of this page, you will understand why Node.js cannot run TypeScript files directly, how ts-node, ESM, and CommonJS interact, and why your Heroku deployment fails when package.json and tsconfig.json use conflicting module systems. You will also learn practical ways to fix the problem: either compile TypeScript to JavaScript before running it, or configure ts-node correctly for the module system you chose.
Concept
TypeScript files use the .ts extension, but Node.js only understands JavaScript at runtime unless you add a tool to translate TypeScript first.
In this question, the main issue is module system mismatch:
package.jsoncontains"type": "module"tsconfig.jsoncontains"module": "commonjs"- the app is started with
ts-node src/App.ts
These settings pull Node in different directions.
What is happening?
Node.js has two main module systems:
- CommonJS: uses
require()andmodule.exports - ES Modules (ESM): uses
importandexport
When you set this in package.json:
{
"type": "module"
}
Node starts treating your project as an ESM project.
Mental Model
Think of Node.js as a person who can only read one language natively: JavaScript.
TypeScript is like a document written in a slightly different language. It looks similar, but it still needs a translator.
tscis a translator that converts the document ahead of timets-nodeis a live interpreter that translates while reading
Now imagine you also tell Node what reading style to use:
"type": "module"says: “Read this as ESM.”"module": "commonjs"says: “Generate CommonJS output.”
That is like giving someone two conflicting instructions:
- “Read this as a modern import/export document”
- “But the translated output will be in an older require/module format”
If the interpreter is not configured to bridge that gap, Node gets confused and stops at the .ts file itself.
Syntax and Examples
Core idea: Node runs JavaScript, not .ts
A common production setup is:
- compile TypeScript with
tsc - run the generated JavaScript with
node
CommonJS-style production setup
package.json
{
"main": "dist/App.js",
"scripts": {
"build": "tsc",
"start": "node dist/App.js",
"dev": "nodemon --exec ts-node src/App.ts"
}
}
tsconfig.json
{
"compilerOptions": {
Step by Step Execution
Consider this setup:
package.json
{
"scripts": {
"build": "tsc",
"start": "node dist/App.js"
}
}
tsconfig.json
{
"compilerOptions": {
"module": "commonjs",
"target": "es2019",
"outDir": "dist"
},
"include": ["src/**/*.ts"]
}
src/App.ts
Real World Use Cases
1. Deploying TypeScript apps to Heroku, Render, or Railway
Many beginners run ts-node locally and assume production should do the same. In production, teams often compile first because it is more predictable.
2. Running API servers
Express, Fastify, NestJS, and other backend apps often use TypeScript during development but run compiled JavaScript in production.
Example pattern:
- local:
ts-node,tsx, or framework dev server - production:
tscthennode dist/server.js
3. CI/CD pipelines
Build servers usually:
- install dependencies
- run tests
- compile TypeScript
- deploy built JavaScript
This avoids runtime differences between environments.
4. Monorepos and shared libraries
Large projects need clear module formats. If one package is ESM and another is CommonJS, build and runtime settings must be aligned carefully.
5. Worker scripts and cron jobs
Background jobs often fail if the environment cannot load .ts files directly. Precompiling avoids that issue.
Real Codebase Usage
In real projects, developers usually pick one of these patterns.
Pattern 1: Compile before start
Very common in production.
{
"scripts": {
"build": "tsc",
"start": "node dist/App.js"
}
}
Why teams like it:
- fewer runtime surprises
- faster startup in production
- easier debugging of deployment issues
- no need for TypeScript execution hooks at runtime
Pattern 2: Separate dev and prod scripts
{
"scripts": {
"dev": "nodemon --exec ts-node src/App.ts",
"build": "tsc",
"start": "node dist/App.js"
}
}
This is a strong beginner-friendly pattern.
Common Mistakes
1. Mixing ESM and CommonJS settings
Broken setup:
{
"type": "module"
}
with:
{
"compilerOptions": {
"module": "commonjs"
}
}
Why it is a problem:
- Node treats files as ESM
- TypeScript compiles as CommonJS
- runtime expectations do not match
How to avoid it:
- either remove
"type": "module" - or switch the TypeScript config and runtime tools to a full ESM setup
2. Running ts-node in production without checking compatibility
Broken assumption:
{
"start": "ts-node src/App.ts"
}
This may work locally but fail in production because of Node version differences or ESM handling.
Comparisons
| Approach | How it works | Pros | Cons | Good for |
|---|---|---|---|---|
ts-node src/App.ts | Runs TypeScript directly at runtime | Fast local development | More runtime/config complexity | Development |
tsc then node dist/App.js | Compiles first, then runs JS | Stable and predictable | Requires build step | Production |
| CommonJS | Uses require() semantics internally | Widely supported, simple in older setups | Less aligned with modern ESM syntax | Many backend apps |
| ESM | Uses native import/ |
Cheat Sheet
Quick rules
- Node does not run
.tsfiles natively ts-nodecan run.ts, but its configuration must match your module system"type": "module"makes Node treat the project as ESM"module": "commonjs"tells TypeScript to output CommonJS- Mixing those settings often causes runtime errors
Safe production setup
package.json
{
"scripts": {
"build": "tsc",
"start": "node dist/App.js"
}
}
tsconfig.json
{
"compilerOptions": {
"module":
FAQ
Why does Node say .ts is an unknown file extension?
Because Node only understands JavaScript by default. A .ts file must be compiled first or handled by a compatible runtime tool such as ts-node.
Why did this happen on Heroku but maybe not locally?
Production environments may use a different Node version or load modules differently. Heroku also runs your start script exactly as defined, so configuration problems become obvious there.
Is ts-node good for production?
It can work, but many teams prefer compiling with tsc and running JavaScript in production because it is simpler and more reliable.
What is wrong with using "type": "module" and "module": "commonjs" together?
They describe different module systems. Node will expect ESM behavior, while TypeScript will compile for CommonJS. That mismatch often causes runtime errors.
Should I remove "type": "module"?
If your project is meant to use CommonJS and you want the simplest fix, yes. Remove it and compile to JavaScript before running.
What is the easiest fix for this specific setup?
Use a build step with tsc, remove "type": "module", and start the compiled file with .
Mini Project
Description
Create a small TypeScript command-line app that starts reliably in both development and production. The project demonstrates the safest beginner-friendly deployment pattern: use ts-node for local development and compile with tsc for production.
Goal
Build a TypeScript app that prints a startup message locally and can be deployed without .ts runtime errors.
Requirements
- Create a
src/App.tsfile that logs a message and the current environment. - Add a development script that runs the TypeScript file directly.
- Add a build script that compiles TypeScript into a
distfolder. - Add a production start script that runs the compiled JavaScript.
- Configure the project so it uses one consistent module system.
Keep learning
Related questions
@Directive vs @Component in Angular: Differences, Use Cases, and When to Use Each
Learn the difference between @Directive and @Component in Angular, including use cases, examples, and when to choose each.
Angular (change) vs (ngModelChange): What’s the Difference?
Learn the difference between Angular (change) and (ngModelChange), when each fires, and which one to use in forms and inputs.
Angular Dependency Injection: Fix "Can't Resolve All Parameters for Component" Errors
Learn why Angular shows "Can't resolve all parameters for component" and how to fix service injection issues in components.