Running TypeScript Files

Node.js · Lesson 26 of 30 8 min read Updated

TypeScript is JavaScript with types: you write price: number or course: "BCA" | "BSc", and the TypeScript compiler warns you about mistakes before the code runs. Many Node.js teams use it, especially on bigger projects. (The TypeScript tutorial teaches the language itself.)

For years, running TypeScript on Node.js needed an extra step: compile .ts files to .js first, or use a tool that compiles on the fly. Node.js 26 can run .ts files directly, with no flag and no extra package. In this lesson you will see how this type stripping works, which TypeScript features it supports, how to set up tsconfig.json to match, and why you still need the TypeScript compiler to check your types.

Running a .ts file

Here are two small TypeScript files for a college fee system:

TypeScript
// fees.ts
export interface Student {
  name: string;
  course: "BCA" | "BSc" | "BTech";
  feesPaid: number;
}

export type FeeStatus = "paid" | "partial" | "due";

export function feeStatus(student: Student, totalFees: number): FeeStatus {
  if (student.feesPaid >= totalFees) return "paid";
  return student.feesPaid > 0 ? "partial" : "due";
}
TypeScript
// app.ts
import { feeStatus } from "./fees.ts";
import type { Student } from "./fees.ts";

const students: Student[] = [
  { name: "Aarav", course: "BCA", feesPaid: 45000 },
  { name: "Priya", course: "BTech", feesPaid: 60000 },
  { name: "Kabir", course: "BSc", feesPaid: 0 },
];

const TOTAL: Record<Student["course"], number> = { BCA: 45000, BSc: 38000, BTech: 120000 };

for (const s of students) {
  console.log(`${s.name.padEnd(6)} ${s.course.padEnd(6)} ${feeStatus(s, TOTAL[s.course])}`);
}
Terminal
node app.ts
Output
Aarav  BCA    paid
Priya  BTech  partial
Kabir  BSc    due

No build step, no dist folder, no extra package. Watch mode (node --watch app.ts) and the test runner work with .ts files too.

How type stripping works

Node.js does not understand types. Instead it erases them: every type annotation, interface, type alias and import type line is replaced with blank space, and what is left is plain JavaScript, which V8 runs. Replacing with spaces (instead of deleting) keeps every line and column in the same place, so error stack traces point to the right spot in your .ts file.

This has two big consequences:

  1. No type checking happens. Node.js removes types; it does not check them.
  2. Only "erasable" TypeScript works. Features that generate JavaScript code cannot simply be erased.

Proof that types are not checked

TypeScript
// price.ts
function addGst(amount: number): number {
  return Math.round(amount * 1.18);
}

console.log(addGst(500));
console.log(addGst("500"));
Output
590
590

The second call passes a string where a number is expected, which is a type error, yet Node.js runs it happily (JavaScript converts "500" to a number when multiplying). In a bigger program, such a mistake leads to bugs far away from the cause. Type checking is still the TypeScript compiler's job.

Checking types with tsc

Install TypeScript as a dev dependency and add a tsconfig.json that matches what Node.js can run:

Terminal
npm install --save-dev typescript @types/node
JSON
{
  "compilerOptions": {
    "noEmit": true,
    "target": "esnext",
    "module": "nodenext",
    "rewriteRelativeImportExtensions": true,
    "erasableSyntaxOnly": true,
    "verbatimModuleSyntax": true,
    "strict": true,
    "types": ["node"]
  }
}
  • noEmit: only check; do not write .js files, because Node.js runs the .ts files itself.
  • module: "nodenext": follow Node.js's own module rules.
  • erasableSyntaxOnly: report TypeScript features that type stripping cannot run.
  • verbatimModuleSyntax: require import type for type-only imports, as Node.js does.
  • rewriteRelativeImportExtensions: allows .ts in import paths.
  • "types": ["node"] with the @types/node package: type information for process and Node.js modules such as node:fs. Current TypeScript versions do not load @types packages unless you list them here.

Now run the checker:

Terminal
npx tsc
Output
bad.ts(1,21): error TS1484: 'Student' is a type and must be imported using a type-only import when 'verbatimModuleSyntax' is enabled.
enum.ts(1,6): error TS1294: This syntax is not allowed when 'erasableSyntaxOnly' is enabled.
price.ts(6,20): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.

The checker found the bad call in price.ts, plus two files (shown below) that Node.js could not run. Add "typecheck": "tsc" to your package.json scripts, run it before every commit, and run it in your automated checks. Your editor (VS Code) also shows these errors as you type.

What is not supported

These TypeScript features produce JavaScript code, so type stripping rejects them with the error code ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX:

FeatureExampleUse instead
enumenum Course { BCA, BSc }A const object with as const, or a union type
namespace with codenamespace Utils { export const x = 1; }ES modules
Parameter propertiesconstructor(private balance: number) {}Declare the field and assign it in the constructor
Import aliasesimport Fs = require("fs")import fs from "node:fs"
Decorators@Controller()Not available without a build tool
.tsx filesReact componentsA bundler or framework

For example, running a file that uses an enum stops at once:

Output
SyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]:   x TypeScript enum is not supported in strip-only mode
   ,-[1:1]
 1 | enum Course { BCA, BSc, BTech }
   : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

The usual replacement for an enum works everywhere:

TypeScript
export const Course = { BCA: "BCA", BSc: "BSc", BTech: "BTech" } as const;
export type Course = (typeof Course)[keyof typeof Course]; // "BCA" | "BSc" | "BTech"

Other rules to remember:

  • Write the .ts extension in imports: import { feeStatus } from "./fees.ts". Without it you get ERR_MODULE_NOT_FOUND.
  • Use import type for types. import { feeStatus, Student } from "./fees.ts" fails at run time with The requested module './fees.ts' does not provide an export named 'Student', because the interface was erased from fees.ts. Write import type { Student } or import { feeStatus, type Student }.
  • tsconfig.json is ignored by Node.js. Settings such as paths aliases have no effect at run time.
  • .ts files follow the "type" field of package.json like .js files; .mts is always an ES module and .cts is always CommonJS.
  • Node.js refuses to strip types from files inside node_modules, so packages must still publish compiled JavaScript.

Common mistakes

  • Thinking Node.js checks types. It does not; run tsc (or your editor) to find type errors.
  • Leaving out .ts in import paths. Node.js does not guess extensions in ES modules.
  • Importing an interface without type. It no longer exists at run time.
  • Using enum or parameter properties. Replace them with as const objects and normal fields, and turn on erasableSyntaxOnly so the compiler warns you.
  • Expecting tsconfig paths aliases to work. Use relative imports or the imports field of package.json.

Key points

  • Node.js 26 runs .ts files directly by erasing types: node app.ts.
  • Type stripping is stable since Node.js 25.2 and 24.12; --experimental-transform-types was removed in 26.
  • Only erasable syntax works: no enum, runtime namespace, parameter properties, decorators or .tsx.
  • Write .ts extensions in imports and use import type for types.
  • Node.js never checks types; use tsc with noEmit, erasableSyntaxOnly and verbatimModuleSyntax.

Exercise

This file does not run with Node.js type stripping. Rewrite it so that it runs with node shop.ts and passes tsc with the settings from this lesson. Keep the same behaviour.

TypeScript
enum Size { Small = "S", Medium = "M", Large = "L" }

class Kurta {
  constructor(public name: string, public size: Size, private price: number) {}
  label(): string {
    return `${this.name} (${this.size}) ₹${this.price}`;
  }
}

console.log(new Kurta("Chikankari kurta", Size.Medium, 1299).label());
Show solution
TypeScript
// shop.ts
const Size = { Small: "S", Medium: "M", Large: "L" } as const;
type Size = (typeof Size)[keyof typeof Size];

class Kurta {
  name: string;
  size: Size;
  #price: number;

  constructor(name: string, size: Size, price: number) {
    this.name = name;
    this.size = size;
    this.#price = price;
  }

  label(): string {
    return `${this.name} (${this.size}) ₹${this.#price}`;
  }
}

console.log(new Kurta("Chikankari kurta", Size.Medium, 1299).label());
Output
Chikankari kurta (M) ₹1299

The enum becomes a const object plus a matching union type, and the parameter properties become normal fields assigned in the constructor. A JavaScript private field (#price) replaces TypeScript's private, and it is private at run time too.

Quick Quiz

1. What does Node.js 26 do with the types in a .ts file?

2. Which TypeScript feature does type stripping NOT support?

3. Why does import { Student } from "./fees.ts" fail when Student is an interface?

4. Which tsconfig.json option makes the compiler report syntax that type stripping cannot run?

Learn Node.js 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