DEV Community

Understanding TypeScript Execution Tools: A Deep Dive

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, and tsx

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 };
Enter fullscreen mode Exit fullscreen mode

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 };
Enter fullscreen mode Exit fullscreen mode

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 expects require / module.exports syntax in .js files.
  • When "type": "module", Node expects import / export syntax in .js files.

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;
}
Enter fullscreen mode Exit fullscreen mode

Intuitively, the caller should pass numbers, but nothing enforces that. Calling

add("a", "b");
Enter fullscreen mode Exit fullscreen mode

…is perfectly valid JavaScript, and it will return:

"ab"
Enter fullscreen mode Exit fullscreen mode

…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;
}
Enter fullscreen mode Exit fullscreen mode

We explicitly state:

  • a must be a number
  • b must be a number
  • the function returns a number

If you try this:

add("hello", "world");
Enter fullscreen mode Exit fullscreen mode

…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
Enter fullscreen mode Exit fullscreen mode

Without this package, TypeScript will complain:

Cannot find module 'fs'
Enter fullscreen mode Exit fullscreen mode

➑️ 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 uses require / module.export syntax
    • "es2015" / "es2020" / "esnext" β†’ output uses import / export syntax

Example tsconfig.json file:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs"
  }
}
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Create hello.ts:

import { join } from "path";
console.log(join("folder", "file.txt"));
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Installation and configuration

To use tsc, first install TypeScript:

npm install --save-dev typescript
Enter fullscreen mode Exit fullscreen mode

Next, generate a tsconfig.json file:

tsc --init
Enter fullscreen mode Exit fullscreen mode

Note: While tsc can compile files without configuration, it's best practice to use a tsconfig.json file.

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

With it, you can simply write:

import express from 'express';
Enter fullscreen mode Exit fullscreen mode

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 any types
  • 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
Enter fullscreen mode Exit fullscreen mode

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"));
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Notes on compiling single files

When you specify file names after tsc:

npx tsc hello.ts
Enter fullscreen mode Exit fullscreen mode

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" in tsconfig.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
Enter fullscreen mode Exit fullscreen mode

(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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Note: ts-node does not include TypeScript.
It is just a wrapper that asks the TypeScript compiler to transpile your file in memory.
Without installing typescript, ts-node will throw:

Error: Cannot find module 'typescript'

Running TypeScript with ts-node

Once installed, run your TypeScript file directly:

npx ts-node hello.ts
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

(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)
Enter fullscreen mode Exit fullscreen mode

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, .js files

…and requires almost no setup.

Installation

First, install tsx:

npm install --save-dev tsx
Enter fullscreen mode Exit fullscreen mode

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-node earlier, uninstall them or start a fresh project so you can clearly see that tsx works without any additional setup.

Running TypeScript with tsx

Simply run your TypeScript file:

npx tsx hello.ts
Enter fullscreen mode Exit fullscreen mode

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: tsx also provides a watch mode for development:

npx tsx watch hello.ts

This automatically reruns your script whenever the file changes.

Summary

For quick reference:

npm install --save-dev tsx @types/node
npx tsx file.ts
Enter fullscreen mode Exit fullscreen mode

(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 .js files

Pros

  • Zero config
  • Just works with ESM and CJS
  • Bundles everything it needs
  • Fastest startup
  • No .js output 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 .js files 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 .js files
  • 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)