iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
📚

Running TypeScript directly in Node.js v22.7.x

に公開

Recent versions of Node.js have introduced experimental support for TypeScript. You can now execute TypeScript scripts without relying on ts-node or tsx.

https://github.com/nodejs/node/pull/53725

Roadmap

https://github.com/nodejs/loaders/issues/217

Understanding that this is an experimental feature, I'll give it a try.

tl;dr

# ~/.zshrc
alias nodets="node --experimental-strip-types --experimental-transform-types --experimental-detect-module --no-warnings=ExperimentalWarning"

# run
nodets run.ts

--experimental-strip-types

This is the simplest implementation, which just strips type annotations.

node --experimental-strip-types run.ts

This allows you to execute code like the following.

const x: number = 1;
console.log(x);

--experimental-transform-types

--experimental-strip-types cannot execute features that go beyond simple removal of type annotations, such as enum, namespace, and module. The --experimental-transform-types flag transforms these features.

node --experimental-strip-types --experimental-transform-types run.ts
const enum E {
	A,
	B,
	C = SubEnum.X,
}
namespace xxx {
	export namespace yyy {
		export const zzz = 123;
	}
}

module a {
	export module b {
		export const c = 456;
	}
}

// See https://zenn.dev/uhyo/articles/typescript-module-option
console.log(E.A, xxx.yyy.zzz, a.b.c);

These features are somewhat like legacy artifacts of TypeScript; honestly, they aren't features I'd actively want to use just because they are now supported. However, you encounter them frequently when porting legacy assets, so it's better to have support than not.

Module Resolution

Since it only strips types, the file resolution rules for Node.js remain unchanged. Therefore, you cannot omit extensions in a module context.

// NG
import { foo } from "./foo";

// OK
import { foo } from "./foo.mts";

As a result, you need to ensure consistency on the tsconfig.json side, for example by using "moduleResolution": "Bundler" or "allowImportingTsExtensions": true so that extensions are not omitted. (Note, however, that Node's current TypeScript support ignores tsconfig.json.)

It also supports .mts and .cts to some extent, but I'll leave the explanation of their behavior to the following article:

https://zenn.dev/uhyo/articles/typescript-module-option

My Use Case

I created a nodets alias and set up these options in it.

# ~/.zshrc
alias nodets="node --experimental-strip-types --experimental-transform-types --experimental-detect-module --no-warnings=ExperimentalWarning"

That said, I wouldn't do something as reckless as running an entire project with this, but it seems like a good fit for writing task scripts like zx's script/ci.ts.

// nodets script/ci.ts
import { $ } from 'zx';

$.verbose = true;
await $`echo "Hello, world!"`;

(I tend to write these kinds of things in Deno.)

If it stops working, I'll fix it myself. Since it's an experimental feature, I won't complain even if it breaks.

Others

The implementation is amaro, an swc-based implementation. Although the arguments become complex, it can be swapped as a loader.

https://github.com/nodejs/amaro

Details on motivation, objectives, etc.: https://github.com/nodejs/node/blob/main/doc/api/typescript.md

Currently, .tsx and source maps are not supported, and it cannot resolve .ts files from other modules in node_modules, so its use cases are limited. However, I think it has made writing zx scripts incredibly easy.

Discussion