Run TypeScript in Node.js
In the previous lesson you compiled TypeScript to JavaScript with tsc and then ran the JavaScript. Modern Node.js can skip that build step: it can run a .ts file directly by removing the types as it loads the file. This is called type stripping.
In this lesson you will run TypeScript files with plain node, learn exactly what type stripping can and cannot handle, and set up tsconfig.json so that the compiler warns you about anything Node.js would reject.
Running a .ts file directly
Create a file called hello.ts in a project whose package.json has "type": "module":
type Order = { item: string; price: number; qty: number };
const order: Order = { item: "Masala dosa", price: 90, qty: 3 };
const total: number = order.price * order.qty;
console.log(`${order.qty} x ${order.item} = ₹${total}`);Now run it:
node hello.ts3 x Masala dosa = ₹270
No tsc, no dist folder, no extra packages. Node.js removed the type Order = ... line and the : Order and : number annotations, then ran what was left as normal JavaScript.
Type stripping arrived in stages:
| Node.js version | Type stripping |
|---|---|
| 22.6 | Added behind the --experimental-strip-types flag |
| 22.18 and 23.6 | Turned on by default |
| 24.12 and 25.2 | Marked stable |
| 26 (current LTS) | Stable and on by default; --experimental-transform-types removed |
On Node.js 22 versions older than 22.18 you must add the flag: node --experimental-strip-types hello.ts. You can turn the feature off with --no-strip-types.
Node.js does not check types
Type stripping only removes types. It never checks them. This file has an obvious type error, yet it runs:
const ticketPrice: number = "free"; // tsc reports an error here
function discount(price: number): number {
return price * 0.9;
}
console.log(ticketPrice);
console.log(discount(ticketPrice));free NaN
The Run button in this course behaves the same way: it removes the types and runs the code. So the rule for real projects is simple: Node.js runs the code, tsc checks it. Keep npx tsc --noEmit (or your editor) in your workflow, for example as an npm script that runs before every commit or deploy.
Erasable syntax only
Node.js replaces each piece of type syntax with blank spaces, so line and column numbers in error messages still match your file. This only works for syntax that can be deleted without changing what the program does. We call that erasable syntax: type annotations, type and interface declarations, generics, as assertions, import type and similar.
A few older TypeScript features generate real JavaScript code, so they cannot simply be deleted:
enumdeclarationsnamespaceblocks that contain values- parameter properties, such as
constructor(public name: string) - import aliases, such as
import fs = require("fs")
Node.js stops with the error ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX when it meets one of these. Decorators also fail, because the JavaScript engine itself does not support them yet. You will learn the modern replacements in Enums & Erasable Syntax and Classes & Access Modifiers.
The erasableSyntaxOnly compiler option (added in TypeScript 5.8) makes tsc report these features as errors, so you find them before Node.js does:
enum Coach { Sleeper, AC3 }
class Seat {
constructor(public no: number) {}
}
namespace Rail { export const zone = "NR"; }bad.ts(1,6): error TS1294: This syntax is not allowed when 'erasableSyntaxOnly' is enabled. bad.ts(4,15): error TS1294: This syntax is not allowed when 'erasableSyntaxOnly' is enabled. bad.ts(7,11): error TS1294: This syntax is not allowed when 'erasableSyntaxOnly' is enabled.
Imports need file extensions and import type
Node.js does not read tsconfig.json, and it does not guess file names. Two rules follow from that.
First, relative imports must include the real file extension, .ts. Second, if you import something that exists only as a type, you must say so with import type (or type inside the braces). Otherwise Node.js looks for a real export with that name and fails, because the type was stripped from the other file.
// fees.ts
export type Student = { name: string; fee: number; paid: number };
export function due(s: Student): number {
return s.fee - s.paid;
}// main.ts
import { due } from "./fees.ts";
import type { Student } from "./fees.ts";
const batch: Student[] = [
{ name: "Aarav", fee: 12000, paid: 12000 },
{ name: "Meera", fee: 12000, paid: 8000 },
];
for (const s of batch) {
console.log(`${s.name} owes ₹${due(s)}`);
}Aarav owes ₹0 Meera owes ₹4000
If you remove the word type from the second import, Node.js fails with SyntaxError: The requested module './fees.ts' does not provide an export named 'Student'. With verbatimModuleSyntax turned on, tsc catches this first with error TS1484.
The module system follows the file extension: .ts files follow the "type" field in package.json (like .js files), .mts files are always ES modules and .cts files are always CommonJS. .tsx files are not supported, and Node.js refuses to strip types from files inside node_modules, so packages must still be published as JavaScript.
A tsconfig that matches Node.js
Use these settings when Node.js runs your .ts files directly. They make tsc a pure checker that agrees with Node.js:
{
"compilerOptions": {
"noEmit": true,
"target": "esnext",
"module": "nodenext",
"rewriteRelativeImportExtensions": true,
"erasableSyntaxOnly": true,
"verbatimModuleSyntax": true,
"types": ["node"]
}
}noEmit: Node.js runs the.tsfiles, sotscdoes not need to write JavaScript.module: nodenext: follow the same module rules as Node.js.rewriteRelativeImportExtensions: allows./fees.tsimports, and if you ever do emit JavaScript, rewrites them to./fees.js.erasableSyntaxOnlyandverbatimModuleSyntax: report anything Node.js would reject.types: ["node"]: load@types/node(install it withnpm install --save-dev @types/node).
Then add "check": "tsc" and "start": "node src/main.ts" to your npm scripts.
Common mistakes
- Thinking
node app.tschecks types. It does not. Runtsc --noEmitas well. - Writing
import { x } from "./utils". Node.js needs the full name./utils.ts. - Importing a type without
type. The import fails at run time because the type no longer exists. - Using
enumorconstructor(public x). These are not erasable; turn onerasableSyntaxOnlyto catch them. - Expecting
pathsaliases to work. Node.js ignorestsconfig.json; use package.jsonimports(names starting with#) instead.
Key points
- Node.js 22.18+ and all current versions run
.tsfiles directly by stripping types; Node.js 26 LTS has it stable and on by default. - Type stripping removes types but never checks them, so keep running
tsc --noEmit. - Only erasable syntax works: no
enum, value namespaces, parameter properties, import aliases or decorators. - Relative imports need the
.tsextension, and type-only imports needimport type. - Use
erasableSyntaxOnly,verbatimModuleSyntax,rewriteRelativeImportExtensionsandmodule: nodenextsotscand Node.js agree.
Exercise
Write marks.ts that exports a type Result (with name and marks) and a function grade(marks: number): string that returns "A" for 80 and above, "B" for 60 to 79 and "C" below that. Write report.ts that imports both correctly and logs the grade of three students: Kabir (85), Isha (72) and Rohan (55). Which import must use type?
Show solution
// marks.ts
export type Result = { name: string; marks: number };
export function grade(marks: number): string {
if (marks >= 80) return "A";
if (marks >= 60) return "B";
return "C";
}// report.ts
import { grade, type Result } from "./marks.ts";
const results: Result[] = [
{ name: "Kabir", marks: 85 },
{ name: "Isha", marks: 72 },
{ name: "Rohan", marks: 55 },
];
for (const r of results) {
console.log(`${r.name}: ${grade(r.marks)}`); // Kabir: A, Isha: B, Rohan: C
}Result exists only as a type, so it is imported with type. Run it with node report.ts.
Quick Quiz
1. What does Node.js do with type annotations when you run node app.ts?
Type stripping removes types without checking them.
2. Which of these works with Node.js type stripping?
A type alias is erasable; enums and parameter properties generate JavaScript code.
3. Why must you write import type { Student } from "./fees.ts"?
Types are stripped, so a normal import of a type fails at run time.
4. Which compiler option reports syntax that Node.js cannot strip?
erasableSyntaxOnly turns enums, parameter properties and value namespaces into compile errors.
Learn TypeScript with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
