I think this + node:test makes Node.js a pretty compelling sensible default for most things now. Running things with `tsx` was such a QoL improvement when it happened, but it didn't solve everything.
Runtime type assertion at the edges is mostly solved through `zod` and tools like `ts-rest` and `trpc` makes it so much easier to do full-stack Typescript these days.
This. It's 2025 and the node ecosystem is finally usable by default!
ESM modules just work with both Node and Typescript, Node can run .ts files, and there's the a good enough test runner built in. --watch. The better built in packages - `node:fs/promises` - are nice with top-level await for easier async loops.
It took a while to convince everyone involved to just be pragmatic, but it's nice now.
What's the story with supporting CommonJS libraries? I've tried to update many projects to ESM multiple times over the years, and every time, I ended up backing out because it turned out that there was some important upstream library that was still CommonJS - or even if we fixed those issues, our downstream NPM consumers wouldn't be able to consume EJS. So then you have to go down this rabbit hole of dual compilation, which actually means using something other than tsc.
This is great to hear, but perhaps comes too late for people like myself. Node.js has been by go-to platform from around 2014 until last year. But around September last year, I found myself thrust into the .NET ecosystem (due to a client project). Within a few months, I realized that it too, had finally become usable by default (unlike the last time I tried it, when it was too tightly coupled to Windows). In fact, it felt like what Node.js would be, if it had strong typing built-in, and had a good standard library that eliminated a lot of the module management and churn. I'm now finding it hard to return to Node.js.
Watching NodeJS fill in these gaps the last 5 years or so has been great, I strongly prefer using built-in stuff as much as possible now to avoid bloating the modules and becoming dependent on a thousand random people being good-stewards of their packages.
I can’t help but think that none of these would have happened without Deno doing it first. It was basically the pragmatic Node before Node started to get reasonable.
I moved on to Biome (which replaces both ESLint and Prettier) and while the IDE extensions have been a bit buggy, it's much faster and has fewer dependencies. It was always a pain to set up ESLint + Prettier.
ESLint these days doesn't have any styling related lints (unless you opt into them) which means that it works out-of-the-box with Prettier (or Biome's formatter, presumably).
My fear with Biome is missing out on type-aware lints, but I know Oxlint has had some success integrating the new Go typescript compiler, so maybe that will work out for Biome as well.
Yes, with the difference that Google would have to be compromised in order to poison the go distributable containing fmt tool. With js, it’s enough to poison any single one of the 1400 dependencies of the linter
I forgot that even though fmt will never suffer from middle man attacks downloading the Go toolchain, the standard library already covers 100% of the uses cases someone cares about using Go for, and no one is using CGO.
I'm very much in favor of TS support directly in node. vitest has made it easier these days, but I've lost too much time over the years getting the balance just right when configuring test environments for .ts files.
trpc and ts-rest are a different animal in my opinion. I'm happy to use either one but won't deal with them in production. For trpc that's mainly due to the lack of owning API URLs and being able to more clearly manage deprecating old URLs gracefully.
For ts-rest I just tend to prefer owning that setup myself, usually with zod and shared typings for API request/response pairs. It also does irk me every time I import what is clearly an RPC tool named "-rest"
It doesn’t. The comment you’re replying to is referring to tsx, the package that lets you execute ts files, not to running files with the tsx extension.
i switched to python a while ago. it has batteries included. i feel so much better now that i dont have to debug all the quirks of a half-baked system.
Last actually note-worthy improvement I heard of was properly supporting import/export (although do you still need to use the .mjs hack?), but I've been out of the loop here for sometime so would be nice to know what they've added since.
Sounds like the obvious correct solution, making .cjs and .mjs obsolete - unless of course someone uses import() statements exclusively, in which case I need to ask: why?
It is surprising for me to see these features finally being added to Node after such a long time. Especially so when I remember reading discussion after discussion about how something like this wasn't possible. I touched on this in a blog post some time ago [1]. Glad Node is catching up.
I don't see in your blogpost any sources cited regarding anyone saying that ES modules were infeasible.
Additionally, io.js actually forked off due to internal drama which started with Ben Noordhuis having changed some pronouns here and there and people wanting to cancel him for that, to which he picked up his toys and left the sandbox.
It so happened that aside from being competent himself, he had competent people on his side, which eventually forced those governing Node.js to concede.
using, memory64, undici, async local storage, ESM import improvements, type stripping, local storage / session storage, env file support, built in file watching. Those are just the ones I mainly remember. There is a lot more.
As a quick aside, “them” is an object pronoun, not a subject pronoun. The correct word you needed is “they”.
You couldn’t phrase your original question as a statement “Them have though.” That’s often a quick test for valid English grammar. With the correct pronoun, it makes more sense: “They have though.”
As another example, take this sentence: “Have you seen them though?”
“You” is the subject of that sentence, and “them” is the object.
It's short for "Have them [Node bozos improved it], though?"
Or, equally likely it, refers to deno and bun ("deno and bun has really made Node focus and improve", "Have them (deno and bun) really made Node focus and improve, though?")
This is great up until you get to the fact that typescript will not be accepted under node_modules [0].
That leads me to ask, what about project dependencies? I wrote a lib for my data models in typescript and I want to import that into my app in node, in typescript? Does the rule only apply to npm packages? There’s opportunity here…
I wrote a runtime in golang that runs typescript (well, JavaScript in general). The grafana folks have sobek that all they need is to add type striping. I feel like if there’s one runtime where typescript could be adopted fully and it would change the world is Node.js. No transpiler, no typescript-go, no rust (well, maybe some rust ;) just a great parser that will keep track of the source map and types in debug mode (for tracing).
Either way, kudos to the node team, contributors, for pulling in the goal posts to make the kick to launch shorter. I’m still a fan of bun, and my own runtime, but node is the standard by which we all are kinda following. I also like that the embedding api is simple and clean to use now so if you want to make an executable, you can.
"Written only in TypeScript" might put it better. If your module ships TypeScript source and a JS build as it should, then this will never affect it. Otherwise, to support stripping arbitrary modules would immediately compromise the design goal of light weight, due to the torrent of ill-founded and -formed bug reports incorrectly raised on Node that would follow. ("Don't make the maintainers' lives too miserable to continue the work" being also of course an implicit goal.)
I can sort of understand the publishing argument, since npm doesn't solve for this at all, unlike JSR:
"You publish TypeScript source, and JSR handles generating API docs, .d.ts files, and transpiling your code for cross-runtime compatibility."
Not allowing it for private modules doesn't make much sense to me, though. It either forces me to use a loader, or now figure out a JS build step which I have been more than happy to avoid up until now.
The implication is that the capability is implemented to conditionally allow, which I suspect has been deliberately avoided, less because the change would require much technical effort (though it might; I don't know) than because the PR to make it offers another opportunity to keep that door nailed firmly shut.
As a past and present module author, I don't feel myself unduly burdened by the need to maintain the tooling to support the work I publish. Or present-ish, anyway; last time around it was Bower and about Node 8, and the experience these days is worlds more comfortable.
I can see why others would feel differently, but again, I'm not really here to argue preferences. If you'd like to fill your afternoon instead with an enjoyable, on-theme read, try the UNIX-HATERS Handbook: https://web.mit.edu/~simsong/www/ugh.pdf
I switched to deno for new projects ~1 year ago and it’s only been joy. There’s a shockingly small amount of friction to switch over, and there are so so many benefits
I'm glad you've said this. I have a project at nearly a perfect point to try out that cutover. Not that it isn't nice to understand the circa 2018-2022 TS stack, but it sure would be nice not to have to. (Our ancestors had the same discussions about cfront(1). Everything old is new again.)
I’m not a heavy JS/TS dev so here’s an honest question: why not use Bun and forget about node? Sure I understand that not every project is evergreen but isn’t Bun a much runtime in general? It supports TS execution from day 1, has much faster dependency resolution, better ergonomics… and I could keep going.
I know I’m just a single data point but I’ve had a lot of success migrating old node projects to bun (in fact I haven’t used node itself since Bun was made public)
Again, I might be saying something terribly stupid because JS/TS isn’t really my turf so please let me know if I’m missing something.
I dislike being on the bleeding edge for things. NodeJS is the most supported in the JS ecosystem. I find it much better to just be on the "default" option for things. You know, choose boring technology.
I've mainly worked with Node for now 8 years, and recently switched to Deno. Even that switch was hard to do; not because things don't work, but you don't know when they won't.
Node has its share of flaws, but it's the de facto baseline against which things are tested and developed. I'm somewhat more comfortable working with The Main Thing.
The JavaScript ecosystem is nightmarish enough that many developers don't want to switch to the Next Cool Thing. I think many of us have had enough fatigue caused by new build tools, new bundlers, new runtimes, etc.
As of right now, Bun is not compelling enough for the potential headaches down the line.
(Maybe there won't be any, but I've spent weeks dealing with incompatibilities caused by a single TS minor update (which should've been breaking). Days chasing after dependency problems, after missing docs, etc.)
I have tried fully switching to bun repeatedly since it came out and every time I got 90% of the way there only to hit a problem that couldn't be worked around. Last I tried I was still stuck on some libraries requiring napi functions that weren't implemented in bun yet, as well an issue I forget but it was vaguely something like `opendir` silently ignoring the `recursive` option causing a huge headache.
I'm waiting patiently for bun to catch up because I would love to switch but I don't think its ready for production use in larger projects yet. Even when things work, a lot of the bun-specific functionality sounds nice at first but feels like an afterthought in practice, and the documentation is far from the quality of node.js
I agree. I've tried the Node TS and test runner features, and they are still (not yet) as good as Bun's. So for now sticking with Bun for those.
Really, in the Node ecosystem you eventually learn not to put all your eggs in one basket. Different things excel in different aspects. Here is my preferred setup for now:
Bun.js:
As a Node runtime, and for TS execution and test running. I tried lots: TSX, TS-Node, Node itself
NPM
For executing tooling scripts
PNPM
For installing dependencies. It's simply better than the rest (npm, yarn, bun) for several reasons
Biome.js
For linting (superior to every other tool I tried)
The bun test runner is definitely a lot better for testing than Node's.
But really, any test runner is beter than Node's: that thing is awful. It's like they looked at all the test runners in existence, and instead of copying what they all did, decided "let's make things harder for no apparent reason."
> I’m not a heavy JS/TS dev so here’s an honest question: why not use Bun and forget about node?
Why would you switch from runtime A to runtime B? I mean, you presented no reason at all, let alone a compelling one, to pick either one. So what leads you to believe it is a reasonable idea to waste time switching runtimes?
For me - it doesn't support secure and reliable dependency vendoring.
The best way to do this atm. is using (and configuring) yarn for zero-installs.
This keeps dependencies inside the codebase so that:
* Issues can be easily traced to the code that actually ran - development and deployment are the same.
* Deployment doesn't depend on package repositories.
* Deployment is secure from many kinds of attacks.
* It is possible to transparently patch packages.
* Development is only internet dependent when adding a new package.
* and the best ease-of-use - no reinstall when changing branches.
Bun is still a toddler: it's not ready for primetime.
Simple example: you know how at the command line you can type "npm run", and then type a character or two, hit tab, and the appropriate script from your `package.json` will autocomplete? And if you keep going (eg. "npm run knex") you can do the same thing to autocomplete arguments?
Bun still hasn't figured out how to do that (https://github.com/oven-sh/bun/issues/6037), even though they can all but copy NPM's (already written) completions. I really liked using bun when I played around with it (and it ran my codebase perfectly, without issue) ... but if they can't handle something as simple as Bash completions, they're clearly not ready for the big leagues.
The best thing is that they are shipping this as "type stripping" which means that there are no sourcemaps involved, making it zero-cost in production!
It also exposes a function which does type stripping (as `import { stripTypeScriptTypes } from 'node:module'`).
This lets you build simple web apps (i.e., those with no frontend dependencies) as pure TypeScript, including the frontend, by stripping the types out from your frontend scripts as you serve them: https://github.com/bakkot/buildless-ts-webapp
One of Typescript's design goals is that removing all type-related parts of the source text should yield a valid JavaScript file. A typescript compiler does not generate code (unlike, say, PureScript).
You can run a typechecker (such as tsc) that check various properties of your code statically, relying on the type information. It is then erased.
The same applies, say, to Python: type annotations are ignored at runtime. Somehow similarly, Java's type information is also partly erased in the bytecode; in particular, all the information about parametrized types. (This is to say nothing about actual machine code.)
This is true but these are also old features, and the TS team have stated that they will not add any more features like those, and to a certain extent regret adding them initially (particularly decorators, which iirc were added because the Angular framework wanted to use them). You can also see that these features aren't really being updated to match recent Typescript developments (parameter properties can't do true private properties, const enums don't work with isolated modules, etc).
I don't think those features are ever going to go away, because they've been around for so long and are so widely used. But I generally use erasableSyntaxOnly in new projects, because I find it's useful when my typescript source matches the generated Javascript code as much as possible.
How useful is it exactly that you accept to not use DX improving syntax like constructor properties, enums, etc? To me, someone who uses these features _a lot_, this would be a terrible trade. Seems more like people push this out of ideology and because TS is never going to be part of node itself (since its implementation is just way too slow)
It's useful as a tool for communication and simplification. Ignoring those features, typescript is just javascript with type annotations, which makes understanding how something works easier. I've worked with a number of developers who have been confused about what's a typescript feature, and what's a javascript feature, because this boundary feels blurred (particularly when working with modern ESNext syntax that might not be implemented in browsers but is understood by typescript). Being able to say clearly: everything after a colon (and some other places) is typescript, everything else is javascript is a great way of helping people understand what's going on.
In terms of the tradeoff, I rarely, if ever, use those features in the first place. For enums, I'd rather just use string literal types, which are slightly easier to write and compose, potentially assigning them to variable names if I think that's clearer for a given use-case. For constructor properties, they don't work for private properties which is the most common property I'm writing, so I don't really see the value.
The two other features that are disabled are namespaces and modules that contain code, and `import ... =`, both of which have clearer JS alternatives.
FWIW, you can enable those features in node, although that's still behind an experimental flag. It's marginally slower (because the translation is slightly more complex than replacing types with whitespace) and it requires sourcemaps under the hood, but if you really want those features you can have them. Or you can use an alternate runtime that supports them out of the box (Deno, Bun). But even then, I think they make teaching and understanding the language more complicated and I wouldn't personally recommend them. (Assuming someone might listen to the personal recommendation of a random HN comment!)
> I find it's useful when my typescript source matches the generated Javascript code as much as possible.
Is this that worth? In the past I was able to read past async/await desugaring generated by TypeScript, and there are several useful non-JS syntaxes that are much easier to read than that (e.g. enums). Of course it would be great if ECMAScript eventually adopts them, but that doesn't seem like a strict requirement for me.
Async/await was implemented according to the ECMAScript spec - for a while it existed in typescript but not yet in browser runtimes, but it was fully specced out as a javascript feature rather than a typescript one. And when configured to emit ESNext JS files, typescript would emit the async/await calls verbatim, rather than transpiling them. After all, at that time async/await was valid, spec-compliant javascript (that hadn't yet been implemented in all browsers).
This is true for a number of features, most recently explicit resource management, that required syntax that was part of ECMAScript, supported in typescript, but not yet implemented in mainstream runtimes. In these cases, typescript is doing the same thing as Babel and converting new JS to old.
The syntaxes under discussion are ones that were implemented in typescript without an ECMAScript proposal, and typically are completely non-standard, and may well never be implemented in browsers, or if they are, be implemented completely differently. For example, the original decorators implementation was completely different to the one now being proposed, and enums could well end up going in the same direction.
This confusion about what is typescript, and what is javascript is exactly why I think avoiding the typescript-only features is a good idea. I've seen a lot of people make this mistake (and it's a very reasonable mistake to make!) and I think it's easier to explain what typescript is actually doing if you make it clear that it's only doing type-level stuff.
Agreed about TS, but Python type annotations are not ignored. They are executed as code (all type annotations are valid expressions) and the results are stored on the module/class/function object that contains the annotated variable
Python type annotations get turned into metadata which other tools may inspect at runtime, but the Python runtime itself does nothing with it. It's just well-structured comments.
In Python basically everything is executable, and so are type annotations.
Somewhat related: you technically can access some type metadata in TypeScript at runtime using the `emitDecoratorMetadata` and `experimentalDecorators` tsconfig options, along with Microsoft's `reflect-metadata` polyfill package. There was a trend at one point where people were writing database ORMs using decorator metadata (e.g. Typegoose, TypeORM, and MikroORM).
This of course requires your build tool to actually understand the TS type system, which is why it's not supported in tools like esbuild and tsx (which uses esbuild under the hood).
>so at best it saves you a transpilation pass and doesn't improve safety.
That's a bit misleading. Node being able to run TS code does not "improve safety", because that's not where the type checking happens. You can do type checking in your editor, or various other points in your toolchain.
Node being able to run TS code reduces the friction in writing TS code, which indirectly helps with type safety.
Except it doesn't. In anything serious, you have to wait for a full type check to happen before you run your TS code. Why would you run code that has not been checked yet and could throw very weird errors like undefined property access?
That just doesn't make sense. Yes, you can wait for your editor in your current open file, if you are lucky and the change in the open file doesn't break anything downstream in another file that is not yet open. In best case you have such simple code that nothing breaks, and in worst case, you have to still run it with type-checking - on top of running it in type-stripping-mode, because you got weird errors in runtime. This is a net negative.
This whole situation is there because we are trying to workaround the slow TSC. It's not a feature, it's something we actively work around. We try to whitewash now the obviously less useful "solution" of running code without its core features enabled: type checking. To me this is insane.
This is not my experience at all. In my experience, it's often quite useful being able to run code that doesn't fully type-check, as long as I do make sure everything's correct by the time I commit it. For example, I might be refactoring a module with some tests, and make a change that breaks the tests and some code in some other module. At this point, I often go in this order:
1. See that the tests aren't type checking correctly (usually for an obvious reason like adding an extra parameter or something).
2. Fix the tests using the type hints.
3. Run the tests to make sure my refactoring made sense and didn't break anything unexpected at runtime.
4. Fix all the uses in other modules.
Step 3 requires me to be able to run code that doesn't type-check correctly, and that's a useful feature.
There's also similar cases where I want to see how something looks in the UI even if it's not properly hooked up yet and causing type errors - I can check that part of the UI works and that it throws the correct runtime error (equivalent to whatever error typescript has). I've also had cases where I've cast things to `unknown` because I don't want to figure out the type just yet, and then written an implementation that is filled with typescript errors but will work at runtime as a mini proof of concept, only to later go back and get the types right.
I shall think you're underestimating how important fast cycle times are. When I'm developing, I normally have my linter, tsc, the dev server (tsx or vite), and the test runner all running simultaneously in watch mode. At any one point, I'm probably only interested in the output from one of these tools (the type checker until the types are all correct, then maybe the test runner until everything's green there). But if I run all of them at once, then they all run optimistically, and the tool I'm interested in is more likely to give me immediate feedback. That's really useful! Even with the new 10x typescript compiler, I'd still rather my tests start running immediately rather than waiting for another process to start and finish before they get going.
You can tsc on the code and then ship that git hash if it passes. You don't need to run it every single time the code executes, nothing of value is gained, because nothing has changed.
TypeScript never promised improving safety, maybe it’s a common misconception. But TypeScript has no runtime mode or information. You were always at the mercy of running and not ignoring the typechecker. Nothing stopped you from running ts-node or tsx on code with egregious type errors. TypeScript is more like a linter in that regard.
I think it's not fair to say that Typescript isn't about improving safety, just that the mechanism isn't the same as with other languages. Typescript had always allowed you to ignore the type checker (in fact, the default configuration will always attempt to emit compiled Javascript, even if the source Typescript has type errors). But if you run the type checker on every commit (via e.g. CI or a precommit hook), then you can be sure that the code you release is correctly typed, which will not guarantee it is safe, but makes it more likely.
I agree that it's better to think of Typescript as a linter that needs specialised annotations to work, rather than a type system like you might find in Java or Rust.
What, pray tell, would be the point of putting all that type information in there, and then have it checked (via tsc), if not for the sake of safety? What other use would this have in your opinion?
No, I don't think there really is. But to be execute is even more clear that it's just... executing the code, whereas I could maybe understand someone being confused that run implied some level of type checking.
This is misleading. It is not transpiling TS in JS, it is transpiling a subset of TS into JS. If my normal TS code can not be "executed" by Node, then it is not executing TS per definition but something else. If you are good with Node supporting and "executing" only a subset of TS and lacking useful features, that's fine. But don't tell people it is executing TypeScript. That's like me saying my rudimentary C++ compiler supports C++ while in reality only supporting 50%. People would be pissed if they figure it out once they try to run it on their codebase.
There is always a compile step (JS -> Bytecode -> Machine code). The question is only if it is visible to you or not. They could have made it totally transparent to you by fully support TS including type checking under the hood including support full TS and not this subset of it, but decided not to do so. There is nothing inherently great to have less compile steps if you are not even aware of it. See v8 how many compile and optimizations steps they have - You don't care, because you don't see it. The only problem of TS is, you will always be able to see it because of it being slow.
I think running TS without type checks is almost entirely pointless.
The point is I don't have to deal with the index.js blob that gets produced by running the compile step myself. Worse yet the source maps. It's significantly less steps so pretty helpful I'd say.
I'm using tsx for a project to achieve the same effect. As you said, it saves you from having to set up a build/transpilation step, which is very useful for development. Tsx has a --watch feature built in as well, which allows me to run a server from the typescript source files and automatically restart on changes. Maybe with nodemon and this new node improvement this can now done without tsx.
To check types at runtime (if that can even be done in a useful way?) it would have to be built into v8, and I suppose that would be a whole rewrite.
Yeah agreed - saying "node can execute typescript files" is a bit misleading. More accurate would be "node can find-and-replace type information with spaces from .ts files and try and executing them as if they were plain JavaScript"
I suspect this would only handle the most rudimentary and basic typescript files. Once you start using the type system more extensively I suspect this will blow-up in your face.
It's kinda a shame. What a missed opportunity to do it properly (rather than relying on third party plugins etc)
Edit: if you are just using typescript for type checking at build time (i.e. the most basic rudimentary typescript files) the sure fine this may help you. But typescript also generates JavaScript code for missing features e.g. proper class access modifiers, generics, interfaces, type aliases, enums, decorators etc etc. typescript generates JS code to handle all that, so you're going to have a bad time if node just replaces it with the space character at run time.
Just to give context here, NodeJS doesnt support enums, namespaces and class parameter properties. All of these have been described as regrets by Anders Hejsberg, and none of them prevent advanced use of the type system at all.
Yes, they regret them because they hinder adoptions. Why? Because nobody chose to add TSC with all features in their runtime because TSC is extremely slow.
They know they can skyrocket adoption by limiting the language. That's the reason they regret it. This is just a strategy to increase adoption. Not because they are bad features. They are in fact very useful, and you should not stop using them just because your favorite runtime decided to go the easy way and only supporting a subset of TS by stripping types. You should rather switch the runtime instead of compromising your codebase.
TypeScript 5.8 added a configuration option[1], where the compiler emits errors when using language features that result in code generation (e.g. enums). It's expected that people wanting to run TypeScript through "erase-only" transpilers will use this option.
Of course, everyone else is free to use enums, decorators, class parameters, etc., but for quick prototyping or writing simple scripts in TypeScript, Bun has been good enough for me, and I assume Node will be "good enough" as well.
> node can find-and-replace type information with spaces from .ts files and try and executing them as if they were plain JavaScript
That’s what all the other tools like ts-node and tsx do already.
I’m not sure what more are you expecting to do?
Typescript is build time type checked, there is no runtime component to TypeScript. If you want type checking you run tsc.
I think this is a great step in the right direction by node. It will save transpiration on the server and improve stack traves and whatnot.
> Once you start using the type system more extensively I suspect this will blow-up in your face.
I don’t see why. There isn’t any more runtime information in “complex”
TypeSceipt types than in simple ones. It’s all build time - see above.
> What a missed opportunity to do it properly
Please explain in more detail what “doing it properly” means to you. Including the typechecker? If so that wouldn’t make sense - they would be competing with TypeScript itself, they shouldn’t, all the “third party plugins” rely on tsc for type checking.
> I think this is a great step in the right direction by node
I think it's the opposite. It will be a net negative, since people will now run TS by default without type checking. Wasting so much time chasing weird runtime errors - just to end up running the full blown TSC type checking again. They will also write very different TS now, trying to workaround the limitation and arguably very useful features like Enums, constructor properties, etc. This has real negative effects on your codebase if you rely on these, just because Node chose to support only a subset.
It's interesting to see the strategy now and to see people even gaslighting people into believing no type checks and less features is a good thing. All just because of one root cause - TSC being extremely slow.
I have been using this feature to remove the transpilation step during development for a rather large monorepo that makes extensive use of very complex types and haven’t noticed any errors whatsoever at runtime.
You’re downplaying this quite a bit. Node being able to execute TS files slashes a lot of tooling in half, especially during development when you want to modify files and retry in quick succession.
Besides, I’m not so sure this cannot be expanded in the future to adopt validation features before stripping the type information. For now it solves some major pain points for a lot of people.
It's not able to execute TypeScript, but a subset of it. The claim in the title is misleading if not totally wrong.
This will unfortunately drive people towards using TS only as a linter, and not use its powerful features that are inherently impossible to implement with just type stripping.
I guess decorators. Which are quite powerful but I’ve never seen them used outside of libraries/frameworks that use them (angular, certain ORMs, nestjs?)
As a personal taste I don’t really like decorators that much, but it’s true that nestjs projects (which is probably a majority of new backend TS projects) will not gain anything from this release. Then again, you always set nestjs up with a template anyway that has all of the tooling and building baked in. So whatevs.
It’s still a huge huge win, and I finally have hope for typescript-ifying some horrible legacy node apps at work!!
Anecdotally Ive noticed a lot of packages failing to build when updating from node 20 to 22.18.0 that weren't failing with earlier node 22 versions. .18 unflagged typescript support.
These are packages using ts-node or tsx to run typescript in node, and with node 22.18 they seem to be using nodes native typescript support instead, and failing due to its limited feature set, or subtly different module resolution.
It performs no type checking, and you don't need to load the compiler to compile. Tsc is a heavy package and having Node do this for you means much faster startup.
I've been using this for helper scripts, where each script is its own entry point, to assist with various maintenance tasks in frontend projects. Much easier to clone the project and run `./scripts/frobnicate` than faff around with tsc. Previously they would have been written in pure JS or just bash.
It's faster and a good bit more convenient in greenfield, in my experience. Less safe in that you do still need a type checker, but nothing about Node's stripping is at all meant for more than experimental use anyway so that's fine.
That is the real reason we get a less feature-rich TypeScript in the future and Node not supporting full TypeScript. Because they want to be supported by browser.
Express is still popular, but a lot projects these days use a full-stack framework like Next.js, SvelteKit, etc.
Fastify, NestJS (bleh), Koa, Hono are the modern replacements for express, none of them have caught on as a standard though. My personal favorite for small projects is Polka (https://github.com/lukeed/polka), when I'm not using Go instead.
IMHO the only reason you are using JS in the backend is because of some meta framework, otherwise is not worth. So at least for Nuxt is nitro, not sure for SvelteKit or the other React meta frameworks.
Next is where it's at these days in my opinion. You get a full-featured client-side React framework (the only one that supports modern React SSG), and then on top of it you get a better-organized approach to doing everything you can do with Express.
And do mean everything: I run an entire Postrgraphile server through Next (and you can easily do the sme with Supabase or a similar tool)!
Contrary to the popular trend, I usually stick with Node and NPM for most of my projects. That said, I’ve run into plenty of headaches with CommonJS vs ESM quirks. Sharing code between frontend and backend (via shared libraries) still feels messy because of those differences. In one project I actually had to switch to Bun as the runtime, since Node kept failing at runtime, and strangely enough, Bun just worked without issues.
Is there any movement in the node world for the runtime to be able to ascertain types? Obviously nodejs doesn't do this, but does Bun or Deno or are there plans for any of this in the future?
This has nothing to do with the "node world". Such an enormous feature would have to go into ECMAScript. Which is very, very unlikely to ever happen, they may as well design a new language. All those runtimes implement that spec. Expecting them to write an extremely complex new feature that is easily more complicated than everything already implemented (especially with backwards compatibility, and given that the language was not designed for this) would be a bit much.
TypeScript is for compile time checking of a language that was not designed to have them. Runtime types have very different requirements! It has to be in the language from the early design phase, otherwise it will just be a hack with many conditions, restrictions and holes.
TS Types are only partially a description of the underlying types in the code, a very big part instead is that it provides guard rails that prevent you from using a lot of perfectly fine and valid JS code that would however be incompatible with type guarantees. You pay the price of using only a part of the large space of JS code possibilities for guarantees. If you were to put that into the runtime you would end up with two different versions of the language. If you still want to support the full JS you would end with two runtimes in one (or one that has so many branches and conditions that maintaining that runtime is a real beast).
Now of course, this would only add type syntax to the language, not true processing: there's nothing in the spec about actually handling them. Still, it's a step in that direction, so I wouldn't say "very unlikely to ever happen" ... "still a long ways off (if ever)" would be more accurate.
Can't be too hard imo. Primitives can contain an extra metadata field for the types defined in the code. This doesn't really interfere with the runtime and will be backwards compatible. The runtime of course still doesn't actually have to do any type checking, it's just forwarding the type information on a new metadata field so it's not in any way interrupting the core flow of the runtime logic. As a result you can still have the wrong type tagged onto a primitive just like TS has it currently.
Only downside I see is that It can slow down the code as the runtime now has to evaluate type level functions in order to know what to place in the metadata.
But you're right in the sense that it has to go into the core ECMA specification rather then being a node project.
i don’t think you’ll see this at the typescript level, and you won’t see anything like this that compiles to javascript. specifically, compiling typescript to javascript with runtime checks would not actually be very useful.
typescript is pretty ambiguous about a lot of the things that would need explicit definition for runtime safety, and anyways we already have tools for that - it’s called zod.
and comprehensive checks would incur a significant runtime penalty, unless they were restricted to external interfaces, which is what you’re really concerned about. we already have tools for that - protobuf, swagger, etc.
anything else is sharing a runtime with you. so either it’s in your ide, and you just don’t write shitty code; or you’re trapped in some kind of demonic javascript prisoner’s dilemma, and you are mutable.
so typescript is basically ‘good enough’ for developers.
thinking forward anyway, and assuming you’re really willing to share a runtime with a stranger…
node doesn’t really operate in that kind of context, but maybe browser code does. i could imagine a framework based on web components, workers, and maybe iframes, taking advantage of message boundaries to enhance analysis and conceal code generation. it’s not that much better than typescript.
but if you want efficient runtime checks, and you want to leverage static analysis and strong module boundaries to scope the type-checking codegen, and you probably need additional syntax, you might as well target wasm.
Yeah, but Zod and all of the runtime schema libraries all kind of add verbosity to the type system compared to say something like Typia[0] which AOT compiles the type checks (and ends up being way more elegant).
Caveat is that there are some restrictions with the compiler and some possible footguns (duplicated declarations bloating code).
I believe it would be too hard to keep up with pace of TypeScript development. We should probably at some point formally define the system and allow for alternative implementations outside of the control of Microsoft.
For one I do not want to run my startup at the mercy of VC funded tech. Node.js is open source and maintained by a foundation, it will not "run out of funds" or be abandoned if there is no profitability in the near future.
Yeah, but it's not a big bet. Deno can do it, Bun can do it, if they die a tragic VC-fueled death then somebody else (maybe Node) can do it. Using Bun to me is just like using a microwave oven in 1980 — there weren't a lot of microwavable convenience foods yet, but you could sure heat up some leftovers more conveniently and quickly.
I think this + node:test makes Node.js a pretty compelling sensible default for most things now. Running things with `tsx` was such a QoL improvement when it happened, but it didn't solve everything.
Runtime type assertion at the edges is mostly solved through `zod` and tools like `ts-rest` and `trpc` makes it so much easier to do full-stack Typescript these days.
This. It's 2025 and the node ecosystem is finally usable by default!
ESM modules just work with both Node and Typescript, Node can run .ts files, and there's the a good enough test runner built in. --watch. The better built in packages - `node:fs/promises` - are nice with top-level await for easier async loops.
It took a while to convince everyone involved to just be pragmatic, but it's nice now.
What's the story with supporting CommonJS libraries? I've tried to update many projects to ESM multiple times over the years, and every time, I ended up backing out because it turned out that there was some important upstream library that was still CommonJS - or even if we fixed those issues, our downstream NPM consumers wouldn't be able to consume EJS. So then you have to go down this rabbit hole of dual compilation, which actually means using something other than tsc.
It's possible, but it can be weird and difficult: https://nodejs.org/docs/latest-v17.x/api/esm.html#esm_common...
Thankfully, actively-maintained CommonJS-only packages are quite rare by this point (in my experience).
> our downstream NPM consumers wouldn't be able to consume EJS
Node.js 20.17 and later supports loading ESM using `require()`: https://nodejs.org/api/modules.html#loading-ecmascript-modul...
The next version of Babel (currently in beta) is even going ESM-only.
It's almost like Python 2 to Python 3 upgrade. Took a decade but everyone is finally happy.
This is great to hear, but perhaps comes too late for people like myself. Node.js has been by go-to platform from around 2014 until last year. But around September last year, I found myself thrust into the .NET ecosystem (due to a client project). Within a few months, I realized that it too, had finally become usable by default (unlike the last time I tried it, when it was too tightly coupled to Windows). In fact, it felt like what Node.js would be, if it had strong typing built-in, and had a good standard library that eliminated a lot of the module management and churn. I'm now finding it hard to return to Node.js.
Interesting. I haven't looked hard at .Net despite some advocacy from past colleagues. Perhaps I should.
Watching NodeJS fill in these gaps the last 5 years or so has been great, I strongly prefer using built-in stuff as much as possible now to avoid bloating the modules and becoming dependent on a thousand random people being good-stewards of their packages.
I can’t help but think that none of these would have happened without Deno doing it first. It was basically the pragmatic Node before Node started to get reasonable.
That's kind of the whole history of NodeJS, dragged-forward kicking and screaming right from the 0.x and IO days!
It's still probably better to use Bun.
Deno 2 is (arguably) just as compelling.
You mean TypeScript. TypeScript is finally usable by default.
I mean Node. I find writing Node in JS intolerable.
ESM, --watch, and TS syntax support is the combo that makes it all good!
nah. it is still eons behind literally everything else
does it have a go fmt / lint command yet?
I moved on to Biome (which replaces both ESLint and Prettier) and while the IDE extensions have been a bit buggy, it's much faster and has fewer dependencies. It was always a pain to set up ESLint + Prettier.
ESLint these days doesn't have any styling related lints (unless you opt into them) which means that it works out-of-the-box with Prettier (or Biome's formatter, presumably).
My fear with Biome is missing out on type-aware lints, but I know Oxlint has had some success integrating the new Go typescript compiler, so maybe that will work out for Biome as well.
Replacing something hard to setup with something buggy is a win?
They've improved, and they will be fine pretty soon.
does that require a config?
depends, will you keep finding pendatic faults after any answer?
Having a typo in "pendantic" is a masterstroke
> "pendantic"
Genius!
One more time and it's pentadic. But imagine someone finding a way to bikeshed anti-bikeshedding tools. "Take with food."
No, it has good defaults. See also: https://prettier.io/docs/option-philosophy
jslint/tslint are an install away.
werent one of the js linters part of a supply chain attack recently?
Maybe, are you sure Go dependencies are immune to similar attacks?
Yes, with the difference that Google would have to be compromised in order to poison the go distributable containing fmt tool. With js, it’s enough to poison any single one of the 1400 dependencies of the linter
I forgot that even though fmt will never suffer from middle man attacks downloading the Go toolchain, the standard library already covers 100% of the uses cases someone cares about using Go for, and no one is using CGO.
Use biome, it doesn't have any external dependencies. eslint should have been put to rest a long time ago.
Good advice. That was my conclusion as well after years of fighting with eslint.
Nope
I'm very much in favor of TS support directly in node. vitest has made it easier these days, but I've lost too much time over the years getting the balance just right when configuring test environments for .ts files.
trpc and ts-rest are a different animal in my opinion. I'm happy to use either one but won't deal with them in production. For trpc that's mainly due to the lack of owning API URLs and being able to more clearly manage deprecating old URLs gracefully.
For ts-rest I just tend to prefer owning that setup myself, usually with zod and shared typings for API request/response pairs. It also does irk me every time I import what is clearly an RPC tool named "-rest"
vitest is incredible; it makes one wonder how/why jest, with its larger user base and community, couldn't get its TS support sorted.
Let's see if Sveltr converts their codebase back to TypeScript
Does this run tsx? Even with the types stripped you still need the JSX transformed to JavaScript
It doesn’t. The comment you’re replying to is referring to tsx, the package that lets you execute ts files, not to running files with the tsx extension.
https://github.com/privatenumber/tsx
i switched to python a while ago. it has batteries included. i feel so much better now that i dont have to debug all the quirks of a half-baked system.
Just wait, you'll find the python pain points at some point.
Two types of languages...
What are the main pain points?
Impressed with what Node is doing the last years, deno and bun has really made Node focus and improve. It was stuck for a while
What are the recent improvements in node itself?
Last actually note-worthy improvement I heard of was properly supporting import/export (although do you still need to use the .mjs hack?), but I've been out of the loop here for sometime so would be nice to know what they've added since.
Here’s a nice overview:
https://kashw1n.com/blog/nodejs-2025/
It doesn’t cover everything, but as an old-school Node user I found several interesting features I didn’t know about.
That is a nice article. It does a great job summing thing up with real examples too.
Small but lovely addition for me is the ability to load .env files natively. There’s more like this; small, focused, real-world-improving features.
> (although do you still need to use the .mjs hack?)
Syntax detection is enabled by default in v22.7.0, v20.19.0:
https://nodejs.org/api/packages.html#syntax-detection
Sounds like the obvious correct solution, making .cjs and .mjs obsolete - unless of course someone uses import() statements exclusively, in which case I need to ask: why?
It is surprising for me to see these features finally being added to Node after such a long time. Especially so when I remember reading discussion after discussion about how something like this wasn't possible. I touched on this in a blog post some time ago [1]. Glad Node is catching up.
[1] https://kilo.bytesize.xyz/an-incorrect-specification
I don't see in your blogpost any sources cited regarding anyone saying that ES modules were infeasible.
Additionally, io.js actually forked off due to internal drama which started with Ben Noordhuis having changed some pronouns here and there and people wanting to cancel him for that, to which he picked up his toys and left the sandbox.
It so happened that aside from being competent himself, he had competent people on his side, which eventually forced those governing Node.js to concede.
Bun is just a cash grab in comparison.
And for a few earlier versions `type: module` in package.json was all that was needed for .js files to be treated as ESM.
using, memory64, undici, async local storage, ESM import improvements, type stripping, local storage / session storage, env file support, built in file watching. Those are just the ones I mainly remember. There is a lot more.
Adding to the list: permissions, CLI styling/colouring, require(esm), globs, test runner.
Do you mean.. node-worthy?
Have them though?
On the projects I am involved, they could even not exist, only node LTS releases matter, and the most recent projects are still node 20.
As a quick aside, “them” is an object pronoun, not a subject pronoun. The correct word you needed is “they”.
You couldn’t phrase your original question as a statement “Them have though.” That’s often a quick test for valid English grammar. With the correct pronoun, it makes more sense: “They have though.”
As another example, take this sentence: “Have you seen them though?”
“You” is the subject of that sentence, and “them” is the object.
Them is fine.
It's short for "Have them [Node bozos improved it], though?"
Or, equally likely it, refers to deno and bun ("deno and bun has really made Node focus and improve", "Have them (deno and bun) really made Node focus and improve, though?")
Your expanded version is also incorrect.
That is nonstandard English, at best. It's found in some uncommon dialects.
Without the expansion I don't know of any native English speaker who would say it.
22 is LTS. The future is now.
All even versions are LTS btw, maybe what you mean is that version 22 entered in maintenance mode (hence stable) and new features will not be added.
Sure, who is going to budget project upgrade effort of ensuring all dependencies work equally as well?
There is a reason why so many Java, Python, .NET/C#, C, C++,.. projects are stuck several versions behind.
No one said developing software wasn't going to be work.
Work isn't free beer in most places.
This is great up until you get to the fact that typescript will not be accepted under node_modules [0].
That leads me to ask, what about project dependencies? I wrote a lib for my data models in typescript and I want to import that into my app in node, in typescript? Does the rule only apply to npm packages? There’s opportunity here…
I wrote a runtime in golang that runs typescript (well, JavaScript in general). The grafana folks have sobek that all they need is to add type striping. I feel like if there’s one runtime where typescript could be adopted fully and it would change the world is Node.js. No transpiler, no typescript-go, no rust (well, maybe some rust ;) just a great parser that will keep track of the source map and types in debug mode (for tracing).
Either way, kudos to the node team, contributors, for pulling in the goal posts to make the kick to launch shorter. I’m still a fan of bun, and my own runtime, but node is the standard by which we all are kinda following. I also like that the embedding api is simple and clean to use now so if you want to make an executable, you can.
[0] https://nodejs.org/api/typescript.html#type-stripping-in-dep...
I made the same comment here https://news.ycombinator.com/item?id=44931575
"To discourage package authors from publishing packages written in TypeScript"
I tried to use it with private packages but that doesn't work either, apparently node doesn't even read the "private" field.
"Written only in TypeScript" might put it better. If your module ships TypeScript source and a JS build as it should, then this will never affect it. Otherwise, to support stripping arbitrary modules would immediately compromise the design goal of light weight, due to the torrent of ill-founded and -formed bug reports incorrectly raised on Node that would follow. ("Don't make the maintainers' lives too miserable to continue the work" being also of course an implicit goal.)
Why would I want to ship a JS build for my private package? That's just extra machinery I don't need. Switching to a superior runtime would be easier.
Well, I don't suppose I know, but if it's an argument you want then that's actually room 12a just next door.
I can sort of understand the publishing argument, since npm doesn't solve for this at all, unlike JSR:
"You publish TypeScript source, and JSR handles generating API docs, .d.ts files, and transpiling your code for cross-runtime compatibility."
Not allowing it for private modules doesn't make much sense to me, though. It either forces me to use a loader, or now figure out a JS build step which I have been more than happy to avoid up until now.
The implication is that the capability is implemented to conditionally allow, which I suspect has been deliberately avoided, less because the change would require much technical effort (though it might; I don't know) than because the PR to make it offers another opportunity to keep that door nailed firmly shut.
As a past and present module author, I don't feel myself unduly burdened by the need to maintain the tooling to support the work I publish. Or present-ish, anyway; last time around it was Bower and about Node 8, and the experience these days is worlds more comfortable.
I can see why others would feel differently, but again, I'm not really here to argue preferences. If you'd like to fill your afternoon instead with an enjoyable, on-theme read, try the UNIX-HATERS Handbook: https://web.mit.edu/~simsong/www/ugh.pdf
It’s a missed opportunity for sure
Compile the packages to JavaScript before publishing. We absolutely should but be publishing TypeScript to npm.
I can understand the argument, since npm has no solution for TypeScript packages, unlike JSR:
"You publish TypeScript source, and JSR handles generating API docs, .d.ts files, and transpiling your code for cross-runtime compatibility."
Still would have been nice to have this for private packages.
This makes Deno/Bun much more attractive alternatives
https://github.com/nodejs/node/issues/57215
Not supporting type stripping in node_modules is unfortunate
But... that's like half the reason why I wanted this feature...
Writing a library in TypeScript (with typechecks in CI/CD as devDependencies) and just importing it directly from Node.js...
It was the first thing I tried and of course it didn't work.
It might finally be time to switch to Deno or Bun =(
I switched to deno for new projects ~1 year ago and it’s only been joy. There’s a shockingly small amount of friction to switch over, and there are so so many benefits
I'm glad you've said this. I have a project at nearly a perfect point to try out that cutover. Not that it isn't nice to understand the circa 2018-2022 TS stack, but it sure would be nice not to have to. (Our ancestors had the same discussions about cfront(1). Everything old is new again.)
Bun has been awesome for me and my team fwiw
Build it.
I mean, you can just use a loader, as we've all been doing. It's already built, they just didn't implement it properly.
No, is the right call. TypeScript has breaking changes. It would need to be standardized first.
What? All this does is strip types
I’m not a heavy JS/TS dev so here’s an honest question: why not use Bun and forget about node? Sure I understand that not every project is evergreen but isn’t Bun a much runtime in general? It supports TS execution from day 1, has much faster dependency resolution, better ergonomics… and I could keep going.
I know I’m just a single data point but I’ve had a lot of success migrating old node projects to bun (in fact I haven’t used node itself since Bun was made public)
Again, I might be saying something terribly stupid because JS/TS isn’t really my turf so please let me know if I’m missing something.
> why not use Bun
I dislike being on the bleeding edge for things. NodeJS is the most supported in the JS ecosystem. I find it much better to just be on the "default" option for things. You know, choose boring technology.
I've mainly worked with Node for now 8 years, and recently switched to Deno. Even that switch was hard to do; not because things don't work, but you don't know when they won't.
Node has its share of flaws, but it's the de facto baseline against which things are tested and developed. I'm somewhat more comfortable working with The Main Thing.
The JavaScript ecosystem is nightmarish enough that many developers don't want to switch to the Next Cool Thing. I think many of us have had enough fatigue caused by new build tools, new bundlers, new runtimes, etc.
As of right now, Bun is not compelling enough for the potential headaches down the line.
(Maybe there won't be any, but I've spent weeks dealing with incompatibilities caused by a single TS minor update (which should've been breaking). Days chasing after dependency problems, after missing docs, etc.)
I have tried fully switching to bun repeatedly since it came out and every time I got 90% of the way there only to hit a problem that couldn't be worked around. Last I tried I was still stuck on some libraries requiring napi functions that weren't implemented in bun yet, as well an issue I forget but it was vaguely something like `opendir` silently ignoring the `recursive` option causing a huge headache.
I'm waiting patiently for bun to catch up because I would love to switch but I don't think its ready for production use in larger projects yet. Even when things work, a lot of the bun-specific functionality sounds nice at first but feels like an afterthought in practice, and the documentation is far from the quality of node.js
I tried to replace Node with Bun, but had the following compatibility problems.
localAddress on TCP connections ignored, last time I tried it its no-op
Incompatibility with Node module APIs (https://github.com/spamscanner/spamscanner wouldn't work)
EventEmitter race problems (partially worked around with https://www.npmjs.com/package/eventemitter2)
Svelte vites dev server sometimes forever freeze until I wiped node_modules and reinstalled it.
I agree. I've tried the Node TS and test runner features, and they are still (not yet) as good as Bun's. So for now sticking with Bun for those.
Really, in the Node ecosystem you eventually learn not to put all your eggs in one basket. Different things excel in different aspects. Here is my preferred setup for now:
Bun.js: As a Node runtime, and for TS execution and test running. I tried lots: TSX, TS-Node, Node itself
NPM For executing tooling scripts
PNPM For installing dependencies. It's simply better than the rest (npm, yarn, bun) for several reasons
Biome.js For linting (superior to every other tool I tried)
The bun test runner is definitely a lot better for testing than Node's.
But really, any test runner is beter than Node's: that thing is awful. It's like they looked at all the test runners in existence, and instead of copying what they all did, decided "let's make things harder for no apparent reason."
> I’m not a heavy JS/TS dev so here’s an honest question: why not use Bun and forget about node?
Why would you switch from runtime A to runtime B? I mean, you presented no reason at all, let alone a compelling one, to pick either one. So what leads you to believe it is a reasonable idea to waste time switching runtimes?
For me - it doesn't support secure and reliable dependency vendoring.
The best way to do this atm. is using (and configuring) yarn for zero-installs.
This keeps dependencies inside the codebase so that: * Issues can be easily traced to the code that actually ran - development and deployment are the same. * Deployment doesn't depend on package repositories. * Deployment is secure from many kinds of attacks. * It is possible to transparently patch packages. * Development is only internet dependent when adding a new package. * and the best ease-of-use - no reinstall when changing branches.
Bun is still a toddler: it's not ready for primetime.
Simple example: you know how at the command line you can type "npm run", and then type a character or two, hit tab, and the appropriate script from your `package.json` will autocomplete? And if you keep going (eg. "npm run knex") you can do the same thing to autocomplete arguments?
Bun still hasn't figured out how to do that (https://github.com/oven-sh/bun/issues/6037), even though they can all but copy NPM's (already written) completions. I really liked using bun when I played around with it (and it ran my codebase perfectly, without issue) ... but if they can't handle something as simple as Bash completions, they're clearly not ready for the big leagues.
The best thing is that they are shipping this as "type stripping" which means that there are no sourcemaps involved, making it zero-cost in production!
Very well done Node team!
It also exposes a function which does type stripping (as `import { stripTypeScriptTypes } from 'node:module'`).
This lets you build simple web apps (i.e., those with no frontend dependencies) as pure TypeScript, including the frontend, by stripping the types out from your frontend scripts as you serve them: https://github.com/bakkot/buildless-ts-webapp
This change finally helped our company move to typescript.
Just converted many of services to TS and some are WIP. It’s a big win.
It looks like this works by stripping away the type information, so at best it saves you a transpilation pass and doesn't improve safety.
One of Typescript's design goals is that removing all type-related parts of the source text should yield a valid JavaScript file. A typescript compiler does not generate code (unlike, say, PureScript).
You can run a typechecker (such as tsc) that check various properties of your code statically, relying on the type information. It is then erased.
The same applies, say, to Python: type annotations are ignored at runtime. Somehow similarly, Java's type information is also partly erased in the bytecode; in particular, all the information about parametrized types. (This is to say nothing about actual machine code.)
> A typescript compiler does not generate code
Except for where it does: Enums, namespaces, parameter properties, etc.
This is true but these are also old features, and the TS team have stated that they will not add any more features like those, and to a certain extent regret adding them initially (particularly decorators, which iirc were added because the Angular framework wanted to use them). You can also see that these features aren't really being updated to match recent Typescript developments (parameter properties can't do true private properties, const enums don't work with isolated modules, etc).
I don't think those features are ever going to go away, because they've been around for so long and are so widely used. But I generally use erasableSyntaxOnly in new projects, because I find it's useful when my typescript source matches the generated Javascript code as much as possible.
> because I find it's useful
How useful is it exactly that you accept to not use DX improving syntax like constructor properties, enums, etc? To me, someone who uses these features _a lot_, this would be a terrible trade. Seems more like people push this out of ideology and because TS is never going to be part of node itself (since its implementation is just way too slow)
It's useful as a tool for communication and simplification. Ignoring those features, typescript is just javascript with type annotations, which makes understanding how something works easier. I've worked with a number of developers who have been confused about what's a typescript feature, and what's a javascript feature, because this boundary feels blurred (particularly when working with modern ESNext syntax that might not be implemented in browsers but is understood by typescript). Being able to say clearly: everything after a colon (and some other places) is typescript, everything else is javascript is a great way of helping people understand what's going on.
In terms of the tradeoff, I rarely, if ever, use those features in the first place. For enums, I'd rather just use string literal types, which are slightly easier to write and compose, potentially assigning them to variable names if I think that's clearer for a given use-case. For constructor properties, they don't work for private properties which is the most common property I'm writing, so I don't really see the value.
The two other features that are disabled are namespaces and modules that contain code, and `import ... =`, both of which have clearer JS alternatives.
FWIW, you can enable those features in node, although that's still behind an experimental flag. It's marginally slower (because the translation is slightly more complex than replacing types with whitespace) and it requires sourcemaps under the hood, but if you really want those features you can have them. Or you can use an alternate runtime that supports them out of the box (Deno, Bun). But even then, I think they make teaching and understanding the language more complicated and I wouldn't personally recommend them. (Assuming someone might listen to the personal recommendation of a random HN comment!)
Enums are crap and unsafe use union types.
> I find it's useful when my typescript source matches the generated Javascript code as much as possible.
Is this that worth? In the past I was able to read past async/await desugaring generated by TypeScript, and there are several useful non-JS syntaxes that are much easier to read than that (e.g. enums). Of course it would be great if ECMAScript eventually adopts them, but that doesn't seem like a strict requirement for me.
Async/await was implemented according to the ECMAScript spec - for a while it existed in typescript but not yet in browser runtimes, but it was fully specced out as a javascript feature rather than a typescript one. And when configured to emit ESNext JS files, typescript would emit the async/await calls verbatim, rather than transpiling them. After all, at that time async/await was valid, spec-compliant javascript (that hadn't yet been implemented in all browsers).
This is true for a number of features, most recently explicit resource management, that required syntax that was part of ECMAScript, supported in typescript, but not yet implemented in mainstream runtimes. In these cases, typescript is doing the same thing as Babel and converting new JS to old.
The syntaxes under discussion are ones that were implemented in typescript without an ECMAScript proposal, and typically are completely non-standard, and may well never be implemented in browsers, or if they are, be implemented completely differently. For example, the original decorators implementation was completely different to the one now being proposed, and enums could well end up going in the same direction.
This confusion about what is typescript, and what is javascript is exactly why I think avoiding the typescript-only features is a good idea. I've seen a lot of people make this mistake (and it's a very reasonable mistake to make!) and I think it's easier to explain what typescript is actually doing if you make it clear that it's only doing type-level stuff.
Just use erasableSyntaxOnly = true and you're fine.
Hadn't heard of that. Thanks
Agreed about TS, but Python type annotations are not ignored. They are executed as code (all type annotations are valid expressions) and the results are stored on the module/class/function object that contains the annotated variable
Python type annotations get turned into metadata which other tools may inspect at runtime, but the Python runtime itself does nothing with it. It's just well-structured comments.
In Python basically everything is executable, and so are type annotations.
Somewhat related: you technically can access some type metadata in TypeScript at runtime using the `emitDecoratorMetadata` and `experimentalDecorators` tsconfig options, along with Microsoft's `reflect-metadata` polyfill package. There was a trend at one point where people were writing database ORMs using decorator metadata (e.g. Typegoose, TypeORM, and MikroORM).
This of course requires your build tool to actually understand the TS type system, which is why it's not supported in tools like esbuild and tsx (which uses esbuild under the hood).
Except in Python you can easily access the type hints at runtime, which allowed people to build ergonomic libraries such as Pydantic
> Somehow similarly, Java's type information is also partly erased in the bytecode;
Just for generics, I believe
>so at best it saves you a transpilation pass and doesn't improve safety.
That's a bit misleading. Node being able to run TS code does not "improve safety", because that's not where the type checking happens. You can do type checking in your editor, or various other points in your toolchain.
Node being able to run TS code reduces the friction in writing TS code, which indirectly helps with type safety.
Except it doesn't. In anything serious, you have to wait for a full type check to happen before you run your TS code. Why would you run code that has not been checked yet and could throw very weird errors like undefined property access?
That just doesn't make sense. Yes, you can wait for your editor in your current open file, if you are lucky and the change in the open file doesn't break anything downstream in another file that is not yet open. In best case you have such simple code that nothing breaks, and in worst case, you have to still run it with type-checking - on top of running it in type-stripping-mode, because you got weird errors in runtime. This is a net negative.
This whole situation is there because we are trying to workaround the slow TSC. It's not a feature, it's something we actively work around. We try to whitewash now the obviously less useful "solution" of running code without its core features enabled: type checking. To me this is insane.
This is not my experience at all. In my experience, it's often quite useful being able to run code that doesn't fully type-check, as long as I do make sure everything's correct by the time I commit it. For example, I might be refactoring a module with some tests, and make a change that breaks the tests and some code in some other module. At this point, I often go in this order:
1. See that the tests aren't type checking correctly (usually for an obvious reason like adding an extra parameter or something). 2. Fix the tests using the type hints. 3. Run the tests to make sure my refactoring made sense and didn't break anything unexpected at runtime. 4. Fix all the uses in other modules.
Step 3 requires me to be able to run code that doesn't type-check correctly, and that's a useful feature.
There's also similar cases where I want to see how something looks in the UI even if it's not properly hooked up yet and causing type errors - I can check that part of the UI works and that it throws the correct runtime error (equivalent to whatever error typescript has). I've also had cases where I've cast things to `unknown` because I don't want to figure out the type just yet, and then written an implementation that is filled with typescript errors but will work at runtime as a mini proof of concept, only to later go back and get the types right.
I shall think you're underestimating how important fast cycle times are. When I'm developing, I normally have my linter, tsc, the dev server (tsx or vite), and the test runner all running simultaneously in watch mode. At any one point, I'm probably only interested in the output from one of these tools (the type checker until the types are all correct, then maybe the test runner until everything's green there). But if I run all of them at once, then they all run optimistically, and the tool I'm interested in is more likely to give me immediate feedback. That's really useful! Even with the new 10x typescript compiler, I'd still rather my tests start running immediately rather than waiting for another process to start and finish before they get going.
You can tsc on the code and then ship that git hash if it passes. You don't need to run it every single time the code executes, nothing of value is gained, because nothing has changed.
TypeScript never promised improving safety, maybe it’s a common misconception. But TypeScript has no runtime mode or information. You were always at the mercy of running and not ignoring the typechecker. Nothing stopped you from running ts-node or tsx on code with egregious type errors. TypeScript is more like a linter in that regard.
I think it's not fair to say that Typescript isn't about improving safety, just that the mechanism isn't the same as with other languages. Typescript had always allowed you to ignore the type checker (in fact, the default configuration will always attempt to emit compiled Javascript, even if the source Typescript has type errors). But if you run the type checker on every commit (via e.g. CI or a precommit hook), then you can be sure that the code you release is correctly typed, which will not guarantee it is safe, but makes it more likely.
I agree that it's better to think of Typescript as a linter that needs specialised annotations to work, rather than a type system like you might find in Java or Rust.
> TypeScript never promised improving safety
What, pray tell, would be the point of putting all that type information in there, and then have it checked (via tsc), if not for the sake of safety? What other use would this have in your opinion?
> TypeScript is more like a linter
that's exactly the point--GP is pointing out that node can't do that part
Which is why the title is "Node can now execute Typescript files" and not lint, check, or even run TypeScript files.
I'm not sure what the distinction between "execute" and "run" is; is there a difference?
No, I don't think there really is. But to be execute is even more clear that it's just... executing the code, whereas I could maybe understand someone being confused that run implied some level of type checking.
Node is just inline transpiling the TS into JS, then running the JS.
This is misleading. It is not transpiling TS in JS, it is transpiling a subset of TS into JS. If my normal TS code can not be "executed" by Node, then it is not executing TS per definition but something else. If you are good with Node supporting and "executing" only a subset of TS and lacking useful features, that's fine. But don't tell people it is executing TypeScript. That's like me saying my rudimentary C++ compiler supports C++ while in reality only supporting 50%. People would be pissed if they figure it out once they try to run it on their codebase.
Not needing a separate compile step just to run some script sounds great to me. I will run tsc if I want a type check.
There is always a compile step (JS -> Bytecode -> Machine code). The question is only if it is visible to you or not. They could have made it totally transparent to you by fully support TS including type checking under the hood including support full TS and not this subset of it, but decided not to do so. There is nothing inherently great to have less compile steps if you are not even aware of it. See v8 how many compile and optimizations steps they have - You don't care, because you don't see it. The only problem of TS is, you will always be able to see it because of it being slow.
I think running TS without type checks is almost entirely pointless.
The point is I don't have to deal with the index.js blob that gets produced by running the compile step myself. Worse yet the source maps. It's significantly less steps so pretty helpful I'd say.
I'm using tsx for a project to achieve the same effect. As you said, it saves you from having to set up a build/transpilation step, which is very useful for development. Tsx has a --watch feature built in as well, which allows me to run a server from the typescript source files and automatically restart on changes. Maybe with nodemon and this new node improvement this can now done without tsx.
To check types at runtime (if that can even be done in a useful way?) it would have to be built into v8, and I suppose that would be a whole rewrite.
Node has had a built-in --watch flag for a while too:
https://nodejs.org/docs/latest/api/cli.html#--watch
Surprisingly this gone unheralded.
I agree, that said if the main reason people use TypeScript is security they should use a decent programming language instead.
Exactly! Type checking can be long and costly.
Ocaml does it fast
Yes, it can't parse enums, for example.
It is available behind an experimental flag: --experimental-transform-types
Typescript mission is to strip itself of typing, what’s your point? Typescript will never be Java, you’ll never have runtime reflection.
Yeah agreed - saying "node can execute typescript files" is a bit misleading. More accurate would be "node can find-and-replace type information with spaces from .ts files and try and executing them as if they were plain JavaScript"
I suspect this would only handle the most rudimentary and basic typescript files. Once you start using the type system more extensively I suspect this will blow-up in your face.
It's kinda a shame. What a missed opportunity to do it properly (rather than relying on third party plugins etc)
Edit: if you are just using typescript for type checking at build time (i.e. the most basic rudimentary typescript files) the sure fine this may help you. But typescript also generates JavaScript code for missing features e.g. proper class access modifiers, generics, interfaces, type aliases, enums, decorators etc etc. typescript generates JS code to handle all that, so you're going to have a bad time if node just replaces it with the space character at run time.
Just to give context here, NodeJS doesnt support enums, namespaces and class parameter properties. All of these have been described as regrets by Anders Hejsberg, and none of them prevent advanced use of the type system at all.
Source: 49m 43s https://m.youtube.com/watch?v=NrEW7F2WCNA
Yes, they regret them because they hinder adoptions. Why? Because nobody chose to add TSC with all features in their runtime because TSC is extremely slow.
They know they can skyrocket adoption by limiting the language. That's the reason they regret it. This is just a strategy to increase adoption. Not because they are bad features. They are in fact very useful, and you should not stop using them just because your favorite runtime decided to go the easy way and only supporting a subset of TS by stripping types. You should rather switch the runtime instead of compromising your codebase.
TypeScript 5.8 added a configuration option[1], where the compiler emits errors when using language features that result in code generation (e.g. enums). It's expected that people wanting to run TypeScript through "erase-only" transpilers will use this option.
Of course, everyone else is free to use enums, decorators, class parameters, etc., but for quick prototyping or writing simple scripts in TypeScript, Bun has been good enough for me, and I assume Node will be "good enough" as well.
[1] https://www.typescriptlang.org/docs/handbook/release-notes/t...
> node can find-and-replace type information with spaces from .ts files and try and executing them as if they were plain JavaScript
That’s what all the other tools like ts-node and tsx do already.
I’m not sure what more are you expecting to do?
Typescript is build time type checked, there is no runtime component to TypeScript. If you want type checking you run tsc.
I think this is a great step in the right direction by node. It will save transpiration on the server and improve stack traves and whatnot.
> Once you start using the type system more extensively I suspect this will blow-up in your face.
I don’t see why. There isn’t any more runtime information in “complex” TypeSceipt types than in simple ones. It’s all build time - see above.
> What a missed opportunity to do it properly
Please explain in more detail what “doing it properly” means to you. Including the typechecker? If so that wouldn’t make sense - they would be competing with TypeScript itself, they shouldn’t, all the “third party plugins” rely on tsc for type checking.
> I think this is a great step in the right direction by node
I think it's the opposite. It will be a net negative, since people will now run TS by default without type checking. Wasting so much time chasing weird runtime errors - just to end up running the full blown TSC type checking again. They will also write very different TS now, trying to workaround the limitation and arguably very useful features like Enums, constructor properties, etc. This has real negative effects on your codebase if you rely on these, just because Node chose to support only a subset.
It's interesting to see the strategy now and to see people even gaslighting people into believing no type checks and less features is a good thing. All just because of one root cause - TSC being extremely slow.
> Typescript is build time type checked, there is no runtime component to TypeScript.
Not exactly. Typescript enums and decorators are examples.
I have been using this feature to remove the transpilation step during development for a rather large monorepo that makes extensive use of very complex types and haven’t noticed any errors whatsoever at runtime.
You’re downplaying this quite a bit. Node being able to execute TS files slashes a lot of tooling in half, especially during development when you want to modify files and retry in quick succession.
Besides, I’m not so sure this cannot be expanded in the future to adopt validation features before stripping the type information. For now it solves some major pain points for a lot of people.
It's not able to execute TypeScript, but a subset of it. The claim in the title is misleading if not totally wrong.
This will unfortunately drive people towards using TS only as a linter, and not use its powerful features that are inherently impossible to implement with just type stripping.
I thought TS abandoned stuff that can't just be stripped. Besides enum, what do you use that isn't strippable?
I guess decorators. Which are quite powerful but I’ve never seen them used outside of libraries/frameworks that use them (angular, certain ORMs, nestjs?)
As a personal taste I don’t really like decorators that much, but it’s true that nestjs projects (which is probably a majority of new backend TS projects) will not gain anything from this release. Then again, you always set nestjs up with a template anyway that has all of the tooling and building baked in. So whatevs.
It’s still a huge huge win, and I finally have hope for typescript-ifying some horrible legacy node apps at work!!
Anecdotally Ive noticed a lot of packages failing to build when updating from node 20 to 22.18.0 that weren't failing with earlier node 22 versions. .18 unflagged typescript support.
These are packages using ts-node or tsx to run typescript in node, and with node 22.18 they seem to be using nodes native typescript support instead, and failing due to its limited feature set, or subtly different module resolution.
But still, bun is the winner here. I recently started using bun and now not moving to node.js again. It just works.
Node still has a better repl, but I do agree. Bun is really good. It’s my goto, too.
Even though this should be the way to go, it still leaves a bitter taste.
Remember ESM fiasco? Now we have a few years of that all over again, this time with different versions of TypeScript, settings, and tsgo. Good news!
> by replacing inline types with whitespace, Node.js can run TypeScript code without the need for source maps.
This is a really neat idea and I hope typescript adds this as a compiler option.
I’m curious what the benefit of stuff like this is vs tsc --watch and running the JS?
I’ve always just run tsc to a .gitignored’d directory and execute my JS from there.
Edit: Thanks for the responses. There’s some great examples in there!
It performs no type checking, and you don't need to load the compiler to compile. Tsc is a heavy package and having Node do this for you means much faster startup.
As a note, Node has a built in --watch now, too
I've been using this for helper scripts, where each script is its own entry point, to assist with various maintenance tasks in frontend projects. Much easier to clone the project and run `./scripts/frobnicate` than faff around with tsc. Previously they would have been written in pure JS or just bash.
It's faster and a good bit more convenient in greenfield, in my experience. Less safe in that you do still need a type checker, but nothing about Node's stripping is at all meant for more than experimental use anyway so that's fine.
Kinda wild this default got flipped on in a minor version. For that matter the TS stripping went in a minor version too
I still want enums
I've tried it, still doesn't work as well as Bun's Typescript and test execution capabilities. So basically just went back to Bun.
I wish browsers also supported directly running typescript files.
Someday! https://github.com/tc39/proposal-type-annotations
That is the real reason we get a less feature-rich TypeScript in the future and Node not supporting full TypeScript. Because they want to be supported by browser.
Already a proposal exists, recent news
That proposal has existed for 4 years, it hasn't gone anywhere yet.
What backend framework is the go to these days? Still Express?
Express is still popular, but a lot projects these days use a full-stack framework like Next.js, SvelteKit, etc.
Fastify, NestJS (bleh), Koa, Hono are the modern replacements for express, none of them have caught on as a standard though. My personal favorite for small projects is Polka (https://github.com/lukeed/polka), when I'm not using Go instead.
IMHO the only reason you are using JS in the backend is because of some meta framework, otherwise is not worth. So at least for Nuxt is nitro, not sure for SvelteKit or the other React meta frameworks.
Next is where it's at these days in my opinion. You get a full-featured client-side React framework (the only one that supports modern React SSG), and then on top of it you get a better-organized approach to doing everything you can do with Express.
And do mean everything: I run an entire Postrgraphile server through Next (and you can easily do the sme with Supabase or a similar tool)!
Wooooo! Finally! <3
does it support watch mode? does it support path aliases?
Great, it does type stripping by replacing TypeScript with whitespace. Can it also now load ES5 modules? That way we can use them for everything.
> Can it also now load ES5 modules?
ES5 standard (released in 2009) didn't have modules.
That answers it. I guess it still can’t! :-/
Quite for some time, assuming they are actually ES6 modules.
Deno has sandboxing and much more - use it instead
Contrary to the popular trend, I usually stick with Node and NPM for most of my projects. That said, I’ve run into plenty of headaches with CommonJS vs ESM quirks. Sharing code between frontend and backend (via shared libraries) still feels messy because of those differences. In one project I actually had to switch to Bun as the runtime, since Node kept failing at runtime, and strangely enough, Bun just worked without issues.
Some previous discussion: https://news.ycombinator.com/item?id=44597966
Is there any movement in the node world for the runtime to be able to ascertain types? Obviously nodejs doesn't do this, but does Bun or Deno or are there plans for any of this in the future?
This has nothing to do with the "node world". Such an enormous feature would have to go into ECMAScript. Which is very, very unlikely to ever happen, they may as well design a new language. All those runtimes implement that spec. Expecting them to write an extremely complex new feature that is easily more complicated than everything already implemented (especially with backwards compatibility, and given that the language was not designed for this) would be a bit much.
TypeScript is for compile time checking of a language that was not designed to have them. Runtime types have very different requirements! It has to be in the language from the early design phase, otherwise it will just be a hack with many conditions, restrictions and holes.
TS Types are only partially a description of the underlying types in the code, a very big part instead is that it provides guard rails that prevent you from using a lot of perfectly fine and valid JS code that would however be incompatible with type guarantees. You pay the price of using only a part of the large space of JS code possibilities for guarantees. If you were to put that into the runtime you would end up with two different versions of the language. If you still want to support the full JS you would end with two runtimes in one (or one that has so many branches and conditions that maintaining that runtime is a real beast).
ECMAScript community is actually dipping their toes in the water of adding typing to the spec, and it's at Stage 1: https://github.com/tc39/proposal-type-annotations.
Now of course, this would only add type syntax to the language, not true processing: there's nothing in the spec about actually handling them. Still, it's a step in that direction, so I wouldn't say "very unlikely to ever happen" ... "still a long ways off (if ever)" would be more accurate.
Can't be too hard imo. Primitives can contain an extra metadata field for the types defined in the code. This doesn't really interfere with the runtime and will be backwards compatible. The runtime of course still doesn't actually have to do any type checking, it's just forwarding the type information on a new metadata field so it's not in any way interrupting the core flow of the runtime logic. As a result you can still have the wrong type tagged onto a primitive just like TS has it currently.
Only downside I see is that It can slow down the code as the runtime now has to evaluate type level functions in order to know what to place in the metadata.
But you're right in the sense that it has to go into the core ECMA specification rather then being a node project.
i don’t think you’ll see this at the typescript level, and you won’t see anything like this that compiles to javascript. specifically, compiling typescript to javascript with runtime checks would not actually be very useful.
typescript is pretty ambiguous about a lot of the things that would need explicit definition for runtime safety, and anyways we already have tools for that - it’s called zod.
and comprehensive checks would incur a significant runtime penalty, unless they were restricted to external interfaces, which is what you’re really concerned about. we already have tools for that - protobuf, swagger, etc.
anything else is sharing a runtime with you. so either it’s in your ide, and you just don’t write shitty code; or you’re trapped in some kind of demonic javascript prisoner’s dilemma, and you are mutable.
so typescript is basically ‘good enough’ for developers.
thinking forward anyway, and assuming you’re really willing to share a runtime with a stranger…
node doesn’t really operate in that kind of context, but maybe browser code does. i could imagine a framework based on web components, workers, and maybe iframes, taking advantage of message boundaries to enhance analysis and conceal code generation. it’s not that much better than typescript.
but if you want efficient runtime checks, and you want to leverage static analysis and strong module boundaries to scope the type-checking codegen, and you probably need additional syntax, you might as well target wasm.
Yeah, but Zod and all of the runtime schema libraries all kind of add verbosity to the type system compared to say something like Typia[0] which AOT compiles the type checks (and ends up being way more elegant).
Caveat is that there are some restrictions with the compiler and some possible footguns (duplicated declarations bloating code).
[0] https://typia.io/
> you won’t see anything like this that compiles to javascript
https://github.com/microsoft/TypeScript/issues/47658
I believe it would be too hard to keep up with pace of TypeScript development. We should probably at some point formally define the system and allow for alternative implementations outside of the control of Microsoft.
the js ecosystem is so sad.
all threads saying the truth, js on the server could implement actual ts and not yet another transpiler gets downvoted.
js "experts" think they are smarter because they know ts is just annotations for a linter. they don't even question why that is so and why that sucks.
I’ve been happy with JSDoc
Bun can do it for years now. I think it's time to move on.
Bun has lots of issues and lots of incompatibilities with a lot of packages.
What are the issues
For one I do not want to run my startup at the mercy of VC funded tech. Node.js is open source and maintained by a foundation, it will not "run out of funds" or be abandoned if there is no profitability in the near future.
Yeah, but it's not a big bet. Deno can do it, Bun can do it, if they die a tragic VC-fueled death then somebody else (maybe Node) can do it. Using Bun to me is just like using a microwave oven in 1980 — there weren't a lot of microwavable convenience foods yet, but you could sure heat up some leftovers more conveniently and quickly.
comparing bun to oven is one of the most clever things that I have seen.
My mind is actually so impressed right now in the sense that bun is well owned by oven.sh company and what you said makes sense...
Do I make sense? Seriously, I can't explain but feel a little mind blown by what you wrote.