# Node runs my TypeScript now (mostly)

Every small TypeScript script I've written for years has needed a helper to run it. ts-node first, then tsx, because it was quicker and didn't fight ESM. It always felt slightly ridiculous to install a package so I could run a forty-line file that renames some CSVs.

But as of July that's mostly over. Type stripping landed behind `--experimental-strip-types` in Node 22.6, was unflagged in [Node 23.6](https://nodejs.org/en/blog/release/v23.6.0) in January, and got backported to the LTS line in 22.18. On a current 22.x or on 24, this just works:

```bash
node scripts/rename-exports.ts
```

You get an `ExperimentalWarning` the first time, which I ignore for scripts. It's worth knowing what Node is doing, because it isn't type-checking anything. It swaps the type annotations for whitespace (so line and column numbers stay put and stack traces are right without source maps) and runs what's left. Which means only syntax that can be erased works. Enums, namespaces with runtime code, parameter properties like `constructor(private db: Db)` and old-style decorators all throw `ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX`. There's `--experimental-transform-types` for those, but I'd rather change the code, and enums were the only ones I actually had:

```ts
// before
export enum Status { Draft = "draft", Live = "live" }

// after
export const Status = { Draft: "draft", Live: "live" } as const;
export type Status = (typeof Status)[keyof typeof Status];
```

Call sites stay as `Status.Live`. Imports caught me out twice, though. A type-only import has to say `import type`, or Node keeps the import and then falls over at runtime when there's nothing there. And you write the real file extension, `./read.ts`, which TypeScript used to hate. `allowImportingTsExtensions` fixes that when you're not emitting, or `rewriteRelativeImportExtensions` (TS 5.7) if you also compile with `tsc`.

## The tsconfig I use for scripts

TypeScript 5.8 added `erasableSyntaxOnly`, which makes the checker complain about exactly the things Node can't strip, so you find out in the editor instead of at runtime. With `verbatimModuleSyntax` forcing the `import type` habit, the pair catches nearly everything:

```json
{
  "compilerOptions": {
    "target": "esnext",
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "noEmit": true,
    "allowImportingTsExtensions": true,
    "erasableSyntaxOnly": true,
    "verbatimModuleSyntax": true,
    "strict": true,
    "skipLibCheck": true
  },
  "include": ["scripts/**/*.ts"]
}
```

A trimmed-down script from one of my side projects, with top-level await, built-in fetch and no dependencies at all:

```ts
// scripts/check-links.ts
import { readFile } from "node:fs/promises";
import type { Post } from "../lib/types.ts";

const posts: Post[] = JSON.parse(await readFile("data/posts.json", "utf8"));

for (const post of posts) {
  const res = await fetch(post.url, { method: "HEAD" });
  if (!res.ok) console.log(`${res.status}  ${post.slug}`);
}
```

Also worth knowing: `require()` of ES modules is unflagged in 22.12 and 20.19, as long as the module doesn't use top-level await. I had an old internal tool pinned to an old version of a library for exactly that reason, and bumping it just worked.

I haven't uninstalled tsx. Anything that runs in production still goes through a proper build with a type-check in CI, and Node doesn't read `paths` from tsconfig, so code importing `@/lib/db` needs a loader (or package.json `imports` like `#lib/db`, which I've started moving to). Stripping also never checks your types, so `tsc --noEmit` stays in CI. Node running the file only tells you it parses.

For scripts, seed files and one-off migrations, though, I just run the `.ts` file now. Not having to explain to a new joiner why they need three packages to run `seed.ts` is a small thing that I'm very happy about.

---

*First published on [my blog](https://blogs.kishankanejiiya.tech/posts/node-runs-typescript-now). Cover photo: Peter Shanks from Hobart, Australia, CC BY 2.0, via Wikimedia Commons ([source](https://commons.wikimedia.org/wiki/File:Green_circuit_board_II_(2389301870).jpg)).*

