Running TypeScript Directly with Node.js: No Build Step Required!

Node.js now supports running TypeScript directly by stripping types at runtime, eliminating the need for a build step. TypeScript 5.8 introduces the erasableSyntaxOnly flag to ensure compatibility. This may streamline workflows and speed up development, although there are limitations to be aware of.

Running TypeScript Directly with Node.js: No Build Step Required!

TypeScript has become the go-to language for JavaScript developers who want type safety and improved tooling. However, one common frustration has been the need for a build step to compile TypeScript to JavaScript before running it. Today, we'll explore the exciting new developments that allow you to run TypeScript directly with Node.js, eliminating that extra compilation step.

The Traditional Way vs. A New Way

Traditionally, working with TypeScript required:

  1. Writing your TypeScript code
  2. Compiling it to JavaScript using tsc or another build tool
  3. Running the compiled JavaScript with Node.js or another JS runtime

This workflow adds complexity and slows down the development process. But thanks to recent advancements in both Node.js and TypeScript, we can now run TypeScript files directly!

Node.js TypeScript Support: Behind the Scenes

Node.js 23.6 unflagged experimental support for running TypeScript files directly. This feature works by stripping types from your TypeScript code at runtime, leaving valid JavaScript that Node.js can execute.

This approach uses a library called Amaro, a wrapper around @swc/wasm-typescript, a WebAssembly port of the SWC TypeScript parser. It's important to note that this differs from full TypeScript compilation - it simply removes the type annotations without performing type checking, a process known as Type Stripping.

TypeScript's New --erasableSyntaxOnly Flag

With TypeScript 5.8, the TypeScript team introduced a new compiler flag called --erasableSyntaxOnly. This flag restricts your code to only use TypeScript features that can be cleanly erased, giving you immediate feedback if something won't work with type-stripping approaches, such as the one Node.js uses.

When enabled, this flag will cause TypeScript to raise errors for:

  • enum declarations
  • namespaces and modules with runtime code
  • Parameter properties in classes (e.g., constructor(private name: string))
  • Non-ECMAScript import = and export = assignments

For example, this code would trigger errors with --erasableSyntaxOnly enabled:

// Error! Not allowed with erasableSyntaxOnly
enum Direction {
  Up,
  Down,
  Left,
  Right,
}

// Error! Not allowed with erasableSyntaxOnly
namespace Container {
  export const value = 42;
}

// Error! Not allowed with erasableSyntaxOnly
import Bar = container.Bar;

class Person {
  // Error! Not allowed with erasableSyntaxOnly
  constructor(private name: string, public age: number) {}
}

Decorator Restrictions

When using Node.js's native TypeScript support, it's important to understand that decorators have significant limitations. Since Node.js is only stripping types without performing transformations, decorators present a special challenge.

Decorators are currently a TC39 Stage 3 proposal and aren't fully supported by JavaScript engines yet. Using Node.js's type-stripping approach, decorators will result in a parser error. This is because decorators require actual code transformation or JS runtime support, thus making them incompatible with type erasure.

Slight differences between TypeScript decorators and the JavaScript proposal further complicate this issue.

For example, this code using decorators won't work with Node.js's type-stripping approach:

function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  descriptor.value = function(...args: any[]) {
    console.log(`Calling ${propertyKey} with`, args);
    return originalMethod.apply(this, args);
  };
  return descriptor;
}

class Calculator {
  @log
  add(a: number, b: number): number {
    return a + b;
  }
}

As the JavaScript decorator proposal progresses and eventually becomes part of the language standard, Node.js will support decorators in TypeScript when supported in JavaScript. Until then, if your project relies heavily on decorators, you must continue using traditional TypeScript compilation workflows.

Setting Up a Project to Run TypeScript Directly

Let's set up a simple project that runs TypeScript directly with Node.js:

💡
Before you begin, use at least Node.js version 23.6 or higher. Older versions may be used with the --experimental-strip-types flag, but weren't tested.
  1. First, create a new directory and initialize a Node.js project:
mkdir ts-direct
cd ts-direct
npm init -y
  1. Install TypeScript as a development dependency:
npm install --save-dev typescript
  1. Create a TypeScript configuration file (tsconfig.json) with the new flag:
touch tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "esModuleInterop": true,
    "strict": true,
    "erasableSyntaxOnly": true,
    "verbatimModuleSyntax": true
  },
  "include": ["src/**/*"]
}
  1. Create a source directory and a simple TypeScript file:
mkdir src
touch src/index.ts
  1. Add a simple TypeScript program to src/index.ts:
// Define types that will be erased at runtime
type Status = "pending" | "success" | "error"

// A function with type annotations
function processItem(id: number, status: Status): string {
  return `Item ${id} is ${status}`
}

// Using the function with proper types
const result = processItem(42, "success")
console.log(result)

// Define a simple class with types
class Item {
  constructor(
    id: number,  // Notice: no access modifiers here!
    status: Status
  ) {
    this.id = id
    this.status = status
  }
  
  id: number
  status: Status
  
  toString(): string {
    return `Item(${this.id}, ${this.status})`
  }
}

const item = new Item(123, "pending")
console.log(item.toString())
  1. Update your package.json to add a start script:
{
  "scripts": {
    "start": "node src/index.ts"
  }
}
  1. Now you can run your TypeScript directly:
npm start
npm start

> ts-direct@1.0.0 start
> node src/index.ts

Item 42 is success
Item(123, pending)
(node:78280) ExperimentalWarning: Type Stripping is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)

Alternative Tools

While Node.js native support is exciting, other established tools exist for directly running TypeScript. A few of these are:

  • ts-node: A TypeScript execution environment for Node.js allows you to run TypeScript code directly.
  • tsx: Another TypeScript execution environment that doesn't perform type checking.
  • esbuild-runner: Uses esbuild for super-fast on-the-fly transpilation of TypeScript.
  • esbuild-node-tsc: Builds your TypeScript Node.js projects using esbuild.

Benefits and Future Outlook

Running TypeScript directly with Node.js offers several advantages:

  • Faster development cycles: No need to wait for compilation
  • Simpler setup: Fewer tools and configuration needed
  • Easier debugging: Debug your TypeScript code directly

This approach also aligns with the "types as comments" proposal for JavaScript, which would allow JavaScript to support type annotations that are safely ignorable at runtime. This could eventually lead to TypeScript support directly in browsers!

Conclusion

The ability to run TypeScript directly with Node.js is a significant step forward for developer experience. By leveraging the new --erasableSyntaxOnly flag in TypeScript 5.8 and Node.js's experimental type-stripping capabilities, we can simplify our workflows while maintaining the benefits of TypeScript's type system.

As these features mature, we can look forward to an even more seamless integration between TypeScript and JavaScript runtimes, potentially bringing us closer to the dream of running TypeScript in the browser without compilation.