When you start exploring TypeScript in Node.js, it's easy to get overwhelmed by all the tools, utilities, and concepts involved: tsx, ts-node, tsc, @types/node, tsconfig.json, CommonJS, and ES Modules. It's not always obvious what each of these does or why they even exist.
This blog post walks you through everything in a logical order:
- A brief history of JavaScript and its module system
- What Node.js is and how it handles modules
- What TypeScript is and how it compiles to JavaScript
- How to run TypeScript files using three different tools:
tsc,ts-node, andtsx
By the end, you'll understand not only how to run .ts files, but why things work the way they do and how to avoid the classic Unexpected token 'export' errors.
Let's get started.
1. A Brief History of JavaScript
JavaScript follows a language specification called ECMAScript. Each version of ECMAScript (ES5, ES6, ES2020β¦) introduces new features to the JavaScript language. Below is a simplified overview of the major versions:
| Name | Year | Notes |
|---|---|---|
| ES3 | 1999 | Very old, still the foundation of modern JavaScript |
| ES5 | 2009 | Big cleanup, added strict mode |
| ES6 / ES2015 | 2015 | Added classes, let, const, and modules (import/export) |
| ES2016βES2023 | yearly | Regular yearly updates with smaller language improvements |
| ESNext | future | The next version; upcoming approved features not yet tied to a single release |
Note: A module in JavaScript is a separate file that exports values (functions, variables, classes) and can be imported by other files. Modules help you break your code into reusable, isolated pieces.
Before ES6 (2015), JavaScript did not have a built-in module system. ES6 introduced native modules with the import / export syntax.
Example:
import fs from "fs";
const file = fs.readFileSync("example.txt", "utf8");
export { file };
2. What is Node.js
Node.js (or "Node" in short) was created in 2009, years before ES6 existed. Node is a JavaScript runtime environment which runs JavaScript outside the browser and provides additional functionality through APIs like fs, path, crypto, etc.
Because modules didn't exist yet, Node invented its own module system: CommonJS (CJS), which uses require() and module.exports syntax.
Example:
const fs = require("fs");
const file = fs.readFileSync("example.txt", "utf8");
module.exports = { file };
When ES6 modules arrived, Node could not support them immediately due to compatibility and ecosystem concerns. Years later, Node eventually added ESM support. But because millions of packages were already written in CommonJS and changing everything would break the ecosystem, Node decided to support both module systems.
How Node decides which module system to use
The type field in package.json tells Node.js how to interpret .js files:
- When it's missing or
"type": "commonjs", Node expectsrequire/module.exportssyntax in.jsfiles. - When
"type": "module", Node expectsimport/exportsyntax in.jsfiles.
This setting affects only how Node interprets .js files, not how TypeScript compiles them (important to remember for later).
Node's dual module support (CommonJS and ES Modules) is one of the main sources of confusion when using TypeScript in Node.js. But now that you understand the historical context, you're ready to learn how TypeScript fits into all of this and how it gets compiled to JavaScript.
3. What is TypeScript
TypeScript (TS) is a typed superset of JavaScript (JS). Let's break down what that really means.
JavaScript example
In plain JavaScript, you might write a function like this:
function add(a, b) {
return a + b;
}
Intuitively, the caller should pass numbers, but nothing enforces that. Calling
add("a", "b");
β¦is perfectly valid JavaScript, and it will return:
"ab"
β¦because JavaScript will convert it to string concatenation.
How TypeScript improves this
TypeScript adds optional static typing, which lets you describe what types values should have.
The same function in TypeScript:
function add(a: number, b: number): number {
return a + b;
}
We explicitly state:
-
amust be a number -
bmust be a number - the function returns a number
If you try this:
add("hello", "world");
β¦TypeScript will give you a compile-time error, before the code runs. Catching mistakes without executing your code is one of the main benefits of TypeScript.
Type annotations in your code
The type annotations in your code are optional, meaning that you can add types but you don't have to. That is, you can write plain JavaScript inside a .ts file and TypeScript will still compile it.
If you omit types, TypeScript will try to infer them (e.g., const x = 5 becomes a number), and if it cannot infer a type, it falls back to the any type.
This allows you to adopt TypeScript's type system gradually at your own pace.
Type definitions for the runtime environment
To analyze your code, TypeScript must not only understand the types in the code you write, but also the types of the APIs provided by the runtime environment (e.g., browser DOM APIs or Node's built-in modules).
There are two environments you'll most commonly use with TypeScript:
Browser environment:
When you install TypeScript, it automatically includes DOM type definitions. These describe the APIs that browsers provide (e.g., window, document, fetch, setTimeout). TypeScript bundles their type definitions by default because many TypeScript projects are browser-based.
β‘οΈ You don't need to install anything extra.
Node environment:
Node provides its own APIs that do not exist in JavaScript or the browser (e.g., fs, path, process). TypeScript does not bundle type defintions for Node by default.
So if you want to use Node APIs in TypeScript (e.g., import fs from "fs"), you must install:
npm install --save-dev @types/node
Without this package, TypeScript will complain:
Cannot find module 'fs'
β‘οΈ Whenever you write TypeScript for Node.js and use Node APIs, you need to install @types/node.
Compilation of TypeScript to JavaScript
Browsers and Node.js cannot run TypeScript directly, they only understand JavaScript. Therefore, TypeScript must be translated ("compiled" / "transpiled") to JavaScript. To do that you need tools like tsc, ts-node or tsx.
These tools "strip out" the TypeScript types and produce valid JavaScript that Node.js or browsers can execute.
How TypeScript decides what JavaScript to emit
TypeScript uses a configuration file called tsconfig.json, located/created in the root of your project, which tells TypeScript how to compile .ts files into .js files.
The two most important settings are:
compilerOptions.target: Defines which version of JavaScript TypeScript should generate (e.g.,ES5,ES2015,ES2020, etc.; see the history table above for all available options).-
compilerOptions.module: Defines which module system the emitted JavaScript should use.-
"commonjs"β output usesrequire/module.exportsyntax -
"es2015"/"es2020"/"esnext"β output usesimport/exportsyntax
-
Example tsconfig.json file:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs"
}
}
This tells TypeScript:
- "Compile my code to JavaScript that matches ES2020 features"
- "Use CommonJS module syntax in the output"
If no tsconfig.json file is provided, TypeScipt defaults to:
{
"compilerOptions": {
"target": "ES3",
"module": "commonjs"
}
}
Puuh... that was a lot of background, but it's all essential context. With this foundation in place, we can now explore the different tools for compiling and running TypeScript.
4. Setup an example project
Let's create a fresh project with a simple TypeScript file:
mkdir my-typescript-project
cd my-typescript-project
npm init -y
Create hello.ts:
import { join } from "path";
console.log(join("folder", "file.txt"));
This uses ESM. But since package.json has no "type": "module", Node expects CommonJS, which creates a mismatch and is expected to result in an error.
Let's see how we can use tsc, ts-node and tsx to execute the scripts and how each of them addresses the mismatch.
5. Executing TypeScript with tsc
tsc is the official TypeScript compiler. It converts TypeScript into JavaScript so Node.js can run it.
Workflow:
hello.ts
β
tsc compiles it to hello.js
β
node runs hello.js
Installation and configuration
To use tsc, first install TypeScript:
npm install --save-dev typescript
Next, generate a tsconfig.json file:
tsc --init
Note: While
tsccan compile files without configuration, it's best practice to use atsconfig.jsonfile.
This creates a configuration file with recommended settings, as shown below (comments and unused options were removed for clarity):
{
"compilerOptions": {
"target": "es2016",
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
}
}
We have already discussed target and module. Here is a brief explanation of the other options:
esModuleInterop: true
Allows importing CommonJS modules (i.e., files using require / module.exports syntax) into ES modules (i.e., files using import / export syntax).
Without it, you'd need:
import * as express from 'express';
With it, you can simply write:
import express from 'express';
This enables interoperability between CommonJS and ES packages.
forceConsistentCasingInFileNames: true
Enforces consistent casing in import paths to prevent issues on case-sensitive file systems. This prevents bugs where ./MyFile and ./myfile would be treated differently.
strict: true
Enables all strict type-checking options for better type safety. It catches:
- implicity
anytypes - null/undefined issues
- incorrect argument types
- uninitialized class properties
- and more.
This is the setting that gives TypeScript its reputation for safety.
skipLibCheck: true
Skips type checking of declaration files (.d.ts) in node_modules to speed up compilation. Your code is still fully type-checked; only third-party library types are skipped.
Compilation
To compile your project, run:
npx tsc
This generates a .js file for each .ts file in your project (in our case, hello.js).
If you inspect hello.js, you will find the following:
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var path_1 = require("path");
console.log((0, path_1.join)("folder", "file.txt"));
You see the require() syntax because module: "commonjs" was set in tsconfig.json.
Run with Node
Now you can run hello.js with Node (note that it's .js, not .ts):
node hello.js
Notes on compiling single files
When you specify file names after tsc:
npx tsc hello.ts
you're invoking tsc in single-file mode.
In this mode, TypeScript ignores tsconfig.json and compiles the specified file(s) with it's built-in defaults (i.e. target: "es3" and module: "commonjs").
Try it out yourself:
- Set
module: "es2020"intsconfig.json - Run
npx tsc hello.ts-> You still get CommonJS syntax in the output
Only if you run npx tsc without file names, it will use the configuration specified in your tsconfig.json file.
Summary
Below the commands to install and execute TypeScript with tsc for quick reference (including installation of @types/node for completeness):
npm install --save-dev typescript @types/node
tsc --init
npx tsc
node file.js
(Replace file.js with the name of your file)
Further reading:
6. Executing TypeScript using ts-node
ts-node is a tool that allows you to run TypeScript files directly, without generating .js files. It transpiles your .ts code in memory and executes it immediately.
Workflow:
hello.ts
β
ts-node transpiles & executes in memory (no .js files are created)
ts-node is great for development, quick scripts, and interactive use. It always respects your tsconfig.json. There are no special cases like in tsc where it may be ignored.
Installation and requirements
To use ts-node, install both ts-node and typescript:
npm install --save-dev ts-node typescript
Note:
ts-nodedoes not include TypeScript.
It is just a wrapper that asks the TypeScript compiler to transpile your file in memory.
Without installingtypescript,ts-nodewill throw:Error: Cannot find module 'typescript'
Running TypeScript with ts-node
Once installed, run your TypeScript file directly:
npx ts-node hello.ts
This:
- Reads your tsconfig.json
- Transpiles the file in memory
- Executes it immediately
No .js file is created.
Summary
Below the commands to install and execute TypeScript with ts-node for quick reference (including installation of @types/node for completeness):
npm install --save-dev ts-node typescript @types/node
tsc --init
npx ts-node file.js
(Replace file.js with the name of your file)
7. Executing TypeScript using tsx (Easiest Option)
tsx is the simplest and most modern way to run TypeScript files.
It runs .ts files directly, without creating .js files, and requires zero configuration in most cases.
Workflow:
hello.ts
β
tsx transpiles & executes in memory (no .js files are created)
The key difference compared to ts-node is that tsx handles many cross-environment details automatically. It works out of the box with:
- ES Modules and CommonJS
- JSX / TSX
- TypeScript path aliases
- JSON imports
-
.mjs,.cjs,.ts,.tsx,.jsx,.jsfiles
β¦and requires almost no setup.
Installation
First, install tsx:
npm install --save-dev tsx
Unlike ts-node, tsx already bundles everything it needs to execute TS files. It does not require installing typescript separately, though most projects will have typescript installed anyway.
If you already installed TypeScript or
ts-nodeearlier, uninstall them or start a fresh project so you can clearly see thattsxworks without any additional setup.
Running TypeScript with tsx
Simply run your TypeScript file:
npx tsx hello.ts
That's it. No tsconfig.json required. No .js output. No module-system headaches.
tsx automatically:
- Detects whether your project uses
"type": "module" - Handles CommonJS and ESM differences
- Applies sensible defaults
- Loads TypeScript configuration if it exists
- Avoids the
Unexpected token 'export'errors that beginners often hit
It just works.
Tip:
tsxalso provides a watch mode for development:npx tsx watch hello.tsThis automatically reruns your script whenever the file changes.
Summary
For quick reference:
npm install --save-dev tsx @types/node
npx tsx file.ts
(Replace file.ts with the TypeScript file you want to run.)
Absolutely β here is a polished and clear section βWhen to Use Whatβ, matching the tone and structure of your existing sections.
8. When to Use What (tsc vs ts-node vs tsx)
You now know three different ways to run TypeScript:
tsc, ts-node, and tsx.
But which one should you actually use?
This section gives you a simple, practical decision guide.
β
Use tsx when you want the easiest, fastest developer experience
tsx is ideal for:
- small scripts
- development environments
- experimentation / prototyping
- running files directly
- building CLIs or dev tools
- any situation where you donβt care about emitted
.jsfiles
Pros
- Zero config
- Just works with ESM and CJS
- Bundles everything it needs
- Fastest startup
- No
.jsoutput clutter - Great watch mode
Cons
- Not a build tool
- Not ideal for production deployment (since nothing is emitted)
Recommendation:
Use tsx for 99% of development and daily scripting.
β
Use ts-node when you need TypeScript execution that respects your project configuration
ts-node is ideal for:
- long-running dev environments
- debugging complex TS applications
- environments that rely heavily on your TypeScript configuration
- older projects that already use
ts-node - scripts/workflows integrated into existing tooling
Pros
- Always respects
tsconfig.json - No
.jsfiles emitted - Supports advanced TS features
- Embedded in many mature TS ecosystems
Cons
- Slower startup than
tsx - Requires installing
typescript - More sensitive to ESM/CJS configuration
- Requires more setup
Recommendation:
Use ts-node when you have a configured TypeScript project and need the compiler settings to be honored exactly.
β
Use tsc when you need actual .js output (production builds)
tsc is ideal for:
- production deployments
- packaging libraries
- creating build artifacts
- publishing to npm
- bundling workflows (paired with tools like esbuild, webpack, tsup, etc.)
Pros
- Produces clean
.jsfiles - Lets Node run without any TS tooling
- Uses static analysis to catch errors before runtime
- Needed for code that must run in production environments
Cons
- Requires a config
- Two-step workflow: compile β execute
- Can be slow without incremental builds
Recommendation:
Use tsc for build steps, production bundles, and anything you want to ship or deploy.
9. Final words
We covered a lot of ground, from JavaScript history, to module systems, to TypeScript configuration, all the way to three different execution tools. With this foundation in place, you're well-positioned to explore more advanced TypeScript features and deepen your understanding at your own pace.
Top comments (0)