Hacker News new | past | comments | ask | show | jobs | submit login
A Node, TypeScript, TS-Node and ESM experience that works (gist.github.com)
198 points by khalidx 5 months ago | hide | past | favorite | 235 comments



The current ESM experience makes it seem like decision-makers in the node.js project were wilfully oblivious to how large the TypeScript community was and how it was being used in node modules and projects. It really does feel like the maintainers focus was on JS and harmony with TypeScript's evolution was low on the priority list.

Meanwhile anyone using an intersection of TypeScript with jest and any of sindresorhus' libraries when he flipped to ESM for his bajillion libraries immediately felt the downside and moved hard away from ESM.

Imagine the mind-boggling hours lost just to get these export/import formats to glue.


I really enjoy frontend/node/typescript development. I roll my eyes whenever the HN-types complain about CSS or frontend development being a hellhole. Mostly the comments I see seem ignorant or impatient ("Why doesn't this thing work without be bothering to learn it?")

However, the intersection of typescript, nodejs, and ES modules is consistently the most frustrating experience I ever have. Trying to figure out which magic incantation of tsconfig/esbuild/tsc/node options will let me just write code and run it is a fools errand. You might figure something out, and then you try to use Jest and then you descend into madness again.

The biggest tip I can give people is to ditch ts-node and just use (the awkwardly named) tsx https://github.com/privatenumber/tsx, which pretty much just "mostly works" for running Typescript during dev for node.

The problem mostly seems to stem for all the stakeholders being pretty dogmatic to whatever their goals are, rather than the pragmatic option of just meeting people where they are. I really wish the Node, Typescript, Deno/Bun, and maybe some bundler people would come together and figure out how to make this easier for people.


Agreed with everything, except this:

> I really wish the Node, Typescript, Deno/Bun, and maybe some bundler people would come together and figure out how to make this easier for people.

Bun has solved this. Bun is straight-up magic; they've implemented tons of hacks and heuristics so everything just works. Bun can even handle ridiculous or otherwise invalid code, like having import and require in the same file.


Unfortunately, bun is unusable due to a myriad of bugs. I closely monitor every bun release, hoping it will function well beyond simple node use cases. The idea is amazing, and I would love to switch to bun, but looking at the issues - no, not yet. How can I trust bun to be a secure runtime with all these bugs?


Which bugs are you referring to? Every runtime has bugs in the issue tracker; you'll have to be more specific than that.


I ran into all types of bugs, including segfaults and incompatibilities with node. After encountering bundler bugs like https://github.com/oven-sh/bun/issues/6168 and https://github.com/oven-sh/bun/issues/4216, I stopped finding workarounds to get bun working.


My point here is that Bun should go teach Node how to be pragmatic and build a good developer experience


Oh, yeah, totally agreed then.


> tons of hacks and heuristics

So, business as usual in the Javascript space. That's how the ecosystem got here, and why nothing works reliably.


Like I said in my comment, Bun works just fine. You seem to have taken 5 words I said out of context to justify something that I didn't say. It's not true that "nothing works reliably". Bun works reliably.


I’ve never heard of bun before today. Why is it so low key and what’s it main selling point?



It's a pretty recent development in the frontend landscape. Its main selling point is a fabulous developer experience. That and, like I said, combing over the ESM mess and basically Just Working (TM).


What are your main sources of tech news? Bun has been quite a darling here for a while..


My own primary news source these days is HN and I’ve missed anything about it, your comment isn’t really helpful.


Bun has solved this, unless you have developers on Windows.


How hard would it be to migrate a node project to Bun though? I want to move, but I'm not sure.


TL;DR: It is too buggy.

I tried it and ran into many issues. For example:

- Incompatibility of transpiled artifacts with the Node runtime (many bugs were fixed, but I haven't tried again). I don't trust bun as a runtime.

- Broken monorepo support.

- No direct react support; you still need react-scripts/webpack/whatever. They have documentation on transpiling react, but it isn't the same.

- No type checking, as with many other tools.

Also, filter their issue tracker for bugs: https://github.com/oven-sh/bun/issues?q=is%3Aissue+is%3Aopen...


Should be a drop-in replacement.


Totally agree.

It's doubly frustrating because a standard for authoring modules across browser and server platforms such as ESM is a good thing. But it's a bit arrogant to expect module authors across TS and JS ecosystems to ship overnight. Beginners may just turn to Deno or Bun simply because hundreds of coding tutorials and snippets no longer work.

Or, when you finally get a TS config that works but then you import @aws-sdk/* or prisma seeds and then you really rip your hair out.


I am finding whatever NextJS does “just works” and I hit issues when I roll my own stuff. But I have’t dug deeply into how they do it yet.


> I roll my eyes whenever the HN-types complain about CSS or frontend development being a hellhole. Mostly the comments I see seem ignorant or impatient ("Why doesn't this thing work without be bothering to learn it?")

I’d also argue that outsiders looking into all the complexity are ignoring the complexity within their own specialization: https://bower.sh/front-end-complexity

It’s hypocritical thinking.


Couldn't agree more. Love the frontend-space, love the ecosystem, but hate the whole ESM vs. CJS fiasco with a passion.

To some degree, I think the typescript-team itself also has to take some blame here. I understand their point that they do not want to do any rewrites, and to some degree it makes sense, but if the ecosystem as a whole really wants to move forward to a common understanding of how it should work, someone needs to do the heavy lifting for dev-experience, and right now they're best-equipped to actually solve the problem, or at the very least help us a lot in doing so.

Their dogmatic approach makes sense for the scope they set out with when starting with typescript but in my eyes refuses a bit the reality the ecosystem currently finds itself in. And I'm saying this as an absolute ts-fanboy; it's one of the very few things about typescript that I take an issue with.


I’m curious what it is you think TypeScript could do, or could do differently, to address the situation. Or what you think they’ve been dogmatic about, and what reality they’ve refused [to see? to accept? to fix? unclear what you mean]?


Hard agree, especially with your first two paragraphs :)

The biggest tip I would add is to bin jest and start using vitest.


For projects where I can, I've moved to Deno and never looked back. It's blissful over there


Except when you need libraries from npm, and then you have a choice 1) use npm: imports and watch things not work 2) use esm.sh imports and watch supposedly-immutable URLs change their contents all the time, and be ready to pile on kludges to get transitive dependencies to behave.


Bartek from the Deno team here. You can also use `package.json` and bare specifiers with Deno. We also recently added `--unstable-byonm` flag (Bring Your Own Node Modules) that allows you to manage `node_modules/` be the package manager of your choice. In other words, you should be able to drop Deno into an existing Node project.


I have had mixed results when using NPM libraries, but I also haven't run into any cases yet where I absolutely needed them

Some stuff you might reach out to NPM for is built into Deno's standard library. Other stuff has become built-in JS language features over the years. And the ecosystem, while nowhere near as big as NPM's, seems to have all the most important stuff at this point, at least for back-end work

And if I have to fill gaps by reinventing some small wheels here and there, or by interning and converting an NPM library, that's still worth it to me to avoid all the tooling headaches


Relieved to hear I’m not the only one. I always blamed myself for not understanding it deeply enough. But admittedly, it is a shit snow and the most frustrating part of development.


How about not using Jest?


Once Bun is feature complete I’m jumping ship


So much this.


TypeScript had years to prepare for ESM, but they did not. Same with Jest. ESM was developed in the open and anyone could participate, including the TypeScript team. You are talking like ESM just happened overnight. It had been in development for 10 years.

Node.js released initial ESM support [1] in Node.js 12.17 in May 2020, 2 years later (!), TypeScript finally added support for ESM [2].

Here's a straight forward guide on how to use TypeScript with ESM: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3...

[1]: https://nodejs.org/en/blog/release/v12.17.0

[2]: https://devblogs.microsoft.com/typescript/announcing-typescr...


> ESM was developed in the open and anyone could participate, including the TypeScript team.

This point stings for me, personally, since _I_ was the TypeScript language dev _in_ this wg trying to make our concerns noted, because we certainly did participate. However the group largely deadlocked on shipping with ecosystem compatibility measures, and what you see in node today is the "minimal core" the group could "agree" on (or be bullied into by group politic - this was getting shipped weather we liked it or not, as the last holdouts). The group was dissolved shortly after shipping it, making said "minimal core" the whole thing (which was the stated goal of some engineers who have since ascended to node maintainer status and are now the primary module system maintainers), with the concerns about existing ecosystem interoperability brought up left almost completely unaddressed. It's been a massive "I told yo so" moment (since a concern with shipping the "minimal core" was that they would never be addressed), but it's not like that helps anyone.

Like this shipped, because _in theory_, it'd be a non-breaking from a library author perspective to get node's CJS to behave reasonably with ESM (...like it does in `bun`, or any one of the bundler-like environments available like `tsx` or `webpack` or `esbuild`), and _in theory_ they're open to a PR for a fix... I wish anyone who tries good luck in getting such a change merged.


Fwiw I appreciate your effort! That sounds really frustrating.

I agree the recent bun/tsx/esbuild (but bun especially) has shown the node CJS/ESM fiasco was a bit of an emperor-wearing-no-clothes moment, where I think us every-day JS programmers just trusted the node devs at their word, that CJS/ESM had to be this painful...

But now, seeing that it doesn't have to be that way, it's like wait a sec...the last ~5 years of pain could have been avoided with some more pragmatic choices? Oof.


> TypeScript had years to prepare for ESM, but they did not.

How did they not? This is confusing to me, as I’ve been authoring and emitting ESM in TS for quite a few years now.


Curious to see how this one decision plays out for nodejs IF -

- module authors increasingly adopt ESM

- TypeScript experience with Deno is butter smooth

- npm packages work with Deno

- performance characteristics are similar

- DX for beginners just works (compared to thousands of stale articles with `require('pkg')` and `npm install pkg@latest` for nodejs)


As someone who uses Jest and alternatively enjoys and is frustrated by it, this isn't all Jest's fault. Node is just now releasing fixes for [significant memory leaks](https://github.com/jestjs/jest/issues/11956#issuecomment-180...) around ESM support within the [VM APIs](https://nodejs.org/api/vm.html) that Jest depends on.


My impression is that Node.js has to be dragged kicking and screaming into any modern JavaScript development practices, and each time, they try to support as little of it as possible as they can get away with.

They have only last year actually rolled out a release that uses ESM modules by default, when the bulk of the community has adopted TypeScript and moved on to using ESM imports (even if they are importing CJS modules under the hood)

They were really late to async/await, and even more so to promises. The chaotic situation where each dependency bundled its own promise library remained in Node.js years after browsers shipped built-in promises. And it still hasn't trickled down to their standard library, most of which is still callback-based and needs to be "promisified" manually. Even their support for fetch, which is eight years old at this point, is still marked as experimental.


Honestly I think the blame[0] is to share across the board. I have read through a bunch of typescript lang issues on this topic, and the general issue is that Typescript doesn't want tsc to do certain kinds of rewrites during compilation (code transpilations that go beyond the most simple things).

This makes sense but honestly means a lot of QoL fixes are kinda impossible to do. At this point I, personally, would almost want tsc (the compiler component) to stop being almost a good bundler and remove some features. Every project I've worked on that uses tsc directly ends up needing a "come to Jesus" moment where a "real" bundler gets introduced and suddenly a lot of stuff becomes easier. Doubly frustrating for me to feel this because tsc does a lot of stuff and it is hard to imagine how Typescript the language moves forward without Typescipt the compiler. I just really don't want to type `.js` for files that are, pre-build, `.ts`.

[0]: "blame" here meant in the weakest sense of "there are people who could make decisions in another way to make this better". Not so much in assigning moral blame


I still don’t fully understand why TypeScript needed to be involved in the discussion at all. TSC tries to do far too much and doesn’t do most of it very well. The tsconfig format is restrictive and annoying.

TS would be a lot better if it were just a type checker and nothing else (which is more or less what you get when you switch to a proper bundler).


I think it's tricky, because if Typescript _didn't_ show up with tsc, then they would have had their fate bound to Webpack or something like that. And of course tsc and the language server go hand in hand. It's hard to understate the positive effects of tsc existing across the entire ecosystem!

I suppose in a way I'm complaining about a (mostly) non-issue, since bundlers all have good typescript support at this point.


Yeah, overall I love TS as a language, and the tooling can be made to work well.

I just feel like there's a bunch of functionality in TSC that's more troublesome than it is useful. The same could be said of Node, of course.


Experienced that this week! Upgraded to node 20 and typescript and his p-limit lib was the source of a lot of pain. No matter how much I changed the tsconfig I just couldn’t get it to work with the subpath imports his lib used. Ended up downgrading the lib to previous major and moving on.


The good news is that one may use `expect` with Node's built-in test runner -- the result feels fairly similar to using expect with Jest.

Indeed, Node's test support can handle dynamic test creation, so one can do crazy things like https://github.com/andrewaylett/prepackage-checks/blob/main/... -- that dynamically asynchronously executes NPM builds from subdirectories, loading and running per-build expectations.


when he flipped to ESM for his bajillion libraries

But that was just a short migration period. It's only been three years and we almost fixed all these issues. This one is probably the last one, this month.


FWIW it wasn't meant to sound critical of him, his contributions are much loved and his attempt to push to ESM was also in good spirit with his helpful gist on the issues, but ESM was no silver bullet for the wide mix of imported modules in some projects.

I remember spending a weekend simply inlining all his module code directly into a project to sidestep the type module change. Fun.


Not fun really. I am critical (of everyone involved). We were asked to migrate under a promise it'll be over soon. "It only needs a little kick", they said, ignoring everyone and vandalizing the loved contributions. Years passed and we:

- still stumble upon it even in fresh projects

- have no esm-only bundle-less projects in practice, because it's still impractical

- learned a bookshelf each about the thing that should just work

- watch entire toolchains decay into a shitton of date-dependent issues

- have no clear troubleshooting paths, even when we have a clue what's wrong

They forcefully promised some bright future, we helplessly agreed.

Where the bright?

Three years later, I lost half an evening to [1] again. My love and politeness reserves are depleted.

[1] https://news.ycombinator.com/item?id=38365983


That it why i use native functions. No typescript syntax, no jest. For testing i use just the native test runner, and that works great with ESM. Jest has indeed still not good ESM support, you can do some Babel trics, but makes the process too complex. Best is to find an alternative, like the native test runner. Also typescript try some things that are not stable at ecma. It was too soon with the import/export, and dont use the native way. Also commonjs is still the default at typescript after compiling, but Node is moving to ESM as a default. I hope typescript will use more native functions that are already available and dont use their own way.

For me typescript syntax and jest is a no go.

Typescript is useful to check types, but i write just JavaScript with JSDoc, and check it with eslint in typescript modus.


Makers of ESM were wilfully oblivious to sanity. CommonJS was/is one of the, if not the, best module and dependency systems out there.

https://gist.github.com/joepie91/bca2fda868c1e8b2c2caf76af7d...


I find this mostly unconvincing (except for the part where all the tools messed this up and made the problem more difficult).

It's not clear to me how you get top-level await with CJS with significant drawbacks OR breaking changes that would just end up back up in ESM land.


The convoluted byzantine mess that is ES top level await is a self-inflicted problem stemming from the ESM bizarre pipe dream that static import won't be a blocking operation for code that depends on the imported code.

With blocking require there's of course no problem with top level await. And having a thenable import doesn't even need any language level support.

Yeah, you can parallelize static imports. But you can just as well doing this by having a semantically blocking require that is free to of course do whatever it wants as long as the execution is sequential in effect.

If there's some dynamic trickery the implementation can't figure out, just revert to blocking and nag in the console.


Being imperative instead of declarative, it hinders a host of key optimizations like parallel loading and dead code elimination


This is discussed in the link I posted. Bundlers manage to statically analyze CommonJS just fine. With require("stringliteral") and exports.thing = thing, that cover 99.9% or so usage, this is just as easy to statically analyze as ESM.

Saying that you need declarative for static analysis is like saying tail call optimization is impossible.

And if a stricter module system would be still required, it could have been quite easy to make compatible with, well, require.


Require is fine but I totally hate the CJS exports. Feels like writing '90s era Perl code. On the other hand ESM dynamic imports like

  import('this').then(r => that()) 
rather suck.


The post dismisses native browser support (a huge benefit of ES modules) as "an utterly useless feature" that's "unavoidably slow" because of the additional roundtrips needed as the dependency tree is traversed.

But this problem has been solved by modulepreload[0], also natively supported in the browser, which lets you specify the modules upfront in the HTML, avoiding the need for additional roundtrips. Tooling could help with generating the list of preloads but is not necessary.

[0] https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes...


Tooling could also analyze the dependencies and create appropriate bundles of the code to avoid roundtrips. Actually such tooling has existed for over a decade and are used by practically all but simplest Javascript applications.

I'm all for development without a compilation step. I've written hacks to resolve node modules and transpile in browser to avoid compilation. Implementing prefetching even with this hack would be less hacky than going full circle and injecting HTML tags to do the prefetching.

ESM is like XML: The problem it solves is not hard, and it doesn't solve the problem well. Actually ESM it isn't even close to solving the problem after trying for almost 15 years.


As you say, tooling can solve the problem. But with ES modules and modulepreload, we can now (contrary to what the post argues) also solve it without the tooling, which is an improvement.

Yes, all but the simplest applications currently use the tooling-based solutions, because there was no other way. But now that we have an alternative solution, perhaps all but the most complex applications will manage just fine without tooling, using just the built-in module support.


So without tooling you need to specify the preload tags by hand. And of course import the modules in the JS.

A simple sync and/or async function/statement would had solved the problem of having modules in the browser. E.g. RequireJS solved this in 2010 or so. There were straightforward proposals for native modules even before this.


> So without tooling you need to specify the preload tags by hand. And of course import the modules in the JS.

Yes. This is exactly what I want. It's helpful for simple apps, and without all the toolchain complexity more apps can be simple.

ES modules solve the problems I care about:

1) Eliminating the extra roundtrips.

2) Nice and simple syntax.

3) Tooling not required.


RequireJS solved 1 and 3 over a decade ago and better than ESM.

2) ESM syntax is objectively more complicated than CJS while managing to be less expressive. It's one of the most complicated module syntaxes out there. Subjectively it's not nice at all, much due to the objective difference.


ESM is a defined default you find back in ECMA specifications. That is why everybody should migratie. Node.je is also moving ESM to the default. If some systems dont do it, dont use it anymore. That it went i dont use jest anymore, and Node.js has also now a good test runner. (Useful for packages and backend systems, i think for frontend systems with eg React there are better test suits to help with special frontend stuff like the DOM)


Many people in this thread are rightly complaining that this does not answer the "why does this work". Having a working example is a good start for that though as at least you have a known target and as you change things (and break things), you can see what those options did.

One problem with this guide that makes that trickier is that the tsconfig.json itself imports https://github.com/sindresorhus/tsconfig/blob/5c87dc118e057d... and overrides a bunch of values.

So if you really want to understand what it's doing, you need to get rid of that dependency and inline / merge that config file. That would also add stability to your project as it's one less upstream dependency out of your control (and a big one at that as it controls how you entire project is built!).

For example the imported tsconfig.json changes these:

    "compilerOptions": {
      // ... 
      "module": "node16",
      "moduleResolution": "node16",
      "moduleDetection": "force",
      // ... 
      "allowSyntheticDefaultImports": true, // To provide backwards compatibility, Node.js allows you to import most CommonJS packages with a default import. This flag tells TypeScript that it's okay to use import on CommonJS modules.
      "jsx": "react",
      // ... 
    }
But you wouldn't notice that if you just look at the gist. It'd just be a magic that you can write React code in a .tsx file. Clearly that has to be enabled somewhere.


https://documentation.divio.com/ is a good overview of the "four types of documentation" paradigm: tutorials, how-to guides, explanations, and reference have to all exist.

One of my major gripes with the JS/TS ecosystem is that "explanations" are sorely lacking. See https://www.typescriptlang.org/tsconfig for the relevant documentation for tsconfig files. Tutorials are on the page, how-to guides abound on the wider internet (like the OP), and the linked TSConfig Reference and JSON Schema (used in code completion in IDEs) are together absolutely massive.

But an explanation is missing! There is no official documentation about how different options interact to say: as I'm walking a file tree as the Typescript compiler, this is how I will interpret a certain file I encounter, what will be outputted, and how that will be interpreted by bundlers and browsers, especially in an ESM world.

https://medium.com/extra-credit-by-guild/tsconfig-json-demys... is in the right direction, but outdated as ESM has become much more popular in the past 3 years, and still organized by option (so it's already somewhat in the "reference" world).

IMO even independent of documentation, the industry's move to ESM is problematic: https://gist.github.com/joepie91/bca2fda868c1e8b2c2caf76af7d... describes many of the issues. But they're certainly exacerbated by good explanation-style documentation that helps people understand how ESM works under the hood!


It's not even that great, typescript doesn't even have a good reference. There are random features that are only documented on release notes.

Working with TS has been somewhat frustrating.


I once did a conference talk on the different types - called it Euclid, Socrates and Mill and analogised a good tutorial to Euclid's Elements, the explanation part to Socrates and howto/cookbook type stuff to John Stewart Mill.

(don't remember if there's a decent video out there but I figure you probably don't need to listen to me waffle to see how the analogies might work ;)


> One of my major gripes with the JS/TS ecosystem is that "explanations" are sorely lacking. See https://www.typescriptlang.org/tsconfig for the relevant documentation for tsconfig files. Tutorials are on the page, how-to guides abound on the wider internet (like the OP), and the linked TSConfig Reference and JSON Schema (used in code completion in IDEs) are together absolutely massive.

> But an explanation is missing! There is no official documentation about how different options interact to say: as I'm walking a file tree as the Typescript compiler, this is how I will interpret a certain file I encounter, what will be outputted, and how that will be interpreted by bundlers and browsers, especially in an ESM world.

Perhaps you missed it, but Andrew (from the TS team) recently finished a massive overhaul of our module docs: https://www.typescriptlang.org/docs/handbook/modules/introdu...

The "theory" page describes TypeScript's perspective on modules. The "reference" page documents things from the "as I'm walking a file tree" perspective (among many other details). The "guides" page also provides recommendations for certain kinds of projects.


I did indeed miss this - thanks so much for the link! Will definitely be digging in here, and hopefully it's helpful to many on this thread!


What programming languages and libraries are well explained in this aspect?


Python, Rust. Given that Python was one of the first open source projects to choose such a model, and Rust explicitly adopted such a model, that's probably not so surprising.

(Somebody is going to bring up the Python packaging clusterfuck as a retort to this, but the fact that Python packaging is both assumed and held at a distance is a larger problem than just documentation)


I am struggling with this just now. I am however writing a library where it makes sense to allow import of subpaths. How would this change then?


I see (and have participated in) a lot of this "just copy this thing, trust me it works" type of content related to Node and TypeScript.

It makes me wonder why Deno [1] still seems to be such a niche choice. Most of these headaches just go away.

Does anyone here prefer Deno for new projects? And if not, why?

[1] And Bun, although that's much newer.


Yeah, I use it for anything new, though so far it's been primarily hobby projects, including a sort of larger one I haven't gotten off the ground yet.

I wouldn't necessarily suggest that a newbie programmer go right to Deno yet because today you still have to be prepared to deal with some differences and quirks.

But here is why I like Deno:

- Typescript pretty much Just Works (TM) out of the box without having to think about it

- More web/browser APIs right out of the box with less Node-specific weirdness. Anything that is Deno-specific (such as anything that lives on the Deno global) is, in my opinion, better designed than Node's APIs.

- Requiring file extensions in import specifiers makes total sense to me. Some people think this is stupid, but I see no reason not to make them actual file paths and not this sort of pseudo file path that omits the extension.

- I like how Deno can bundle your application into a single executable.

- Testing framework and linter are built right in. Which is great because, let's face it, most of us have been using such things for a very long time now.

- Being able to pull in modules from the web without NPM is fantastic.

- Node.js and NPM compatibility has gotten way better recently.

- Binary data is handled as Uint8Array rather than using a non-standard Buffer like in Node. Makes total sense. Skip the middle-man and just give me the bytes in a widely compatible form.

- Granular security with strict permissions by default is really nice. Sometimes I do want to import a module but then have a guarantee that third-party code won't phone home. It's also flexible enough that you can allow your own code to reach your own domains but then some module you import from elsewhere doesn't send data to some other domain. I'd still choose to not use such modules, but it's good that you can mitigate the risk of malicious code without having to do anything.

And sure, Node has improved a lot since the inception of Deno and will continue to improve. It can probably do some of these things and may end up having more parity with Deno. I don't think that Node is bad. I still use Node, primarily at work, and in cases where a package has maintainers that refuse to support Deno (ex. Playwright).


I prefer Deno over Python now for "cross-platform shell scripting", because the "import directly from a github repository" is very nice for that use case. Haven't tried it yet as direct replacement for npm/node.js though (mainly because I'm unsure yet how well it works as a drop-in replacement when other people continue using npm and node.js).


I've tried both, and love their potential.

I would highly advise any startup considering using them to reconsider. You don't want your business to being running into rough edges all day. You don't want to be reinventing things all day.

They're fun, and especially deno is really fun. It feels more like go, where you control everything.

But they're still not nearly as productive as node


Can you expand on what you mean by "not nearly as productive as node"? I've found the opposite to be true, because of the simplified tooling.

That said, most of my Deno projects have been fairly basic, so I'm curious if there's a point at which the early wins give way to hidden pain points.


I would love to use Deno, however it is not totally compatible with the TypeScript ecosystem: it is not possible to import a TypeScript file using the `js` extension. This is a dealbreaker for me because by adopting the `ts` extension you make your code deno-specific; You can no longer use TSC, ESbuild and the like.


Bartek from the Deno team here. We are working on making this possible with a flag in the upcoming release.


Great to know!


Not sure what you mean? Our whole codebase is .ts files, and we use tsc and esbuild without apparent difficulty.


I mean, if you have a file `mod.ts`, in Deno you cannot import `mod.ts` using the following import:

  import {} from "./mod.js"
You have to write:

  import {} from "./mod.ts"
ESbuild doesn't transform source imports. Thus, by transpiling your code using ESbuild, you get an erroneous import that leads to a runtime error in browsers and Node. The case is worse for TypeScript, because it refuses to tarnspile this code.

By the way, bundling with ESbuild is possible, however when you write a library you generally want to transpile the code, not to bundle it.


I use esbuild to import .ts regularly, I'm not sure how you came to that conclusion


ESbuild correctly resolves the path, however it doesn't rewrite the import path. Thus, you end with a javascript file that tries to import a typescript file (this leads to a runtime error in the browser and Node).


We don’t put extensions on imports

    import {} from "./mod“;
and we bundle before running. Maybe that explains it.


Yes. I transpile my libraries and in order to be ESM-compatible you have to use the extension of the emitted files (in my case `.js`)


ESbuild has handled “.ts” imports for as long as I’ve used it.

TSC recently added a flag that allows “.ts” imports too.


> ESbuild has handled “.ts” imports for as long as I’ve used it.

Yes, ESbuild correctly resolves the path, however it doesn't rewrite the import path. Thus, you end with a javascript file that tries to import a typescript file (this leads to a runtime error in the browser and Node).

> TSC recently added a flag that allows “.ts” imports too.

For type-checking only. You cannot emit code when using `ts` extension.


Ah, yes, I'm assuming the code will be bundled (so all the imports will be completely rewritten anyway).

Is there a good use case where you wouldn't want to bundle TS code?

It seems to me that you either want to execute it directly (via Deno / Bun / ts-node / etc) or bundle it.

Even if you're publishing an NPM package, bundling rather than publishing individual generated .js files works well. You can either bundle your dependencies or leave them unbundled, both work just fine.

If you publish your .ts files, people can import those directly. In my experience that works much better in Visual Studio Code than importing the .js files.


Because I am publishing libraries. Generally you don't want to bundle a library, you want to transpile it. You leave the bundling phase to an application.

Transpiling instead of bundling avoids some pitfalls. Just to cite one that comes to mind: If your library has a file`a.ts` importing a file `b.ts` including the statement `export * as X from "c.js"`, you could end up with a bundled file that includes an object `X` with the exported elements of `c.js`. This prevents tree-shaking when you bundle applications that use this library.

Transpiling (or just publishing js source files if you don't use TypeScript), also allows a better debugging experience (without relying on source map).

Another point: it is currently not possible to bundle TypeScript declaration files with a regular bundle. If you bundle your sources, you end with a distribution folder with a bundled file and a myriad of lonely declaration files. This is ugly and I am not sure if TypeScript is happy with that?

However, bundling offer also some advantages compared to transpiling: the publication size is smaller, you have less compatibility issues with Deno. Maybe I will switch to bundling my libraries if it doesn't reduce optimization when bundling an application, and when bundling declarations files will be available among the bundlers.

> If you publish your .ts files, people can import those directly. In my experience that works much better in Visual Studio Code than importing the .js files.

Do you know some projects which publish their ts sources?


Prefer not to use Deno.

First, my personal choice is not to use TypeScript. For me it just adds a layer of complexity that adds very little. That I am a sole developer certainly is a factor in that decision. And history. I worked with Java for a long time, with all the safety it provides, only to find that PHP and JavaScript programmers could ship in 1/4 the time with the same level of functionality. I find that passing named parameters: `foo({animal: "duck"})` makes my plain old javascript much more robust, with very little cost.

So strike #1 against Deno is that it puts typescript much more in the foreground.

Strike #2 is that Node works. Not because it is right, but because services make sure they work with node. They don't put the same effort into making things work with Deno.

I needed to write code using Deno that talked to a Digital Ocean managed Postgres database. I spent three days. Then I decided not to use Deno. Was this Deno's fault? No. But if you need to get things done and one in 1000 of those things works in Node but not in Deno, then the choice is straight forward.

I'm not saying these are good reasons, but I did try Deno and wanted it to work.


While I mostly agree with you, I hope deno removes at least part of the nodejs mess when becomes ready for prime time. It's already better designed in so many ways. Then I can safely ignore the typescript functionality that, like you said, ads too little value for me. This is mostly wishful thinking. If deno doesn't deliver, maybe bun will. The funny thing is they are trying to improve deno by adding compatibility to the same nodeisms that make node such a messy experience, like package.json and node_modules. Once these will get used out of habbit, there is no going back.


This may be a luddite-esque take, but personally, I'm very hesitant to use Bun on any meaningful project at the moment. It screams too-good-to-be-true right now. I also believe that I may have heard of some people running across issues with it recently, although I maybe conflating my memory of it with something else. That, and, while I understand the namespace pollution with JS packages, saying that I use "Bun" for my projects is a bit of a turn-off, to be quite honest. I may try it for some pet project in the near-term future but if I was to make a choice for anything else I see operating long term I'd be much more inclined to use Deno, although I still think even that is probably a bit less established then I'd prefer, so I'd probably still use Node until Deno matures a bit more.


i think its reasonable to hesitate on using something less than 2 years old. it does a lot of things that look good to me, but im happy to wait a few years and see. it still needs to get buy in from people who maintain packages, for example just try using puppeteer with it. and its also not clear to me how its going to make money.


Because deno is strongly opinionated on things other than the language and module system. I don’t want to use golang style dependencies. I don’t want all the upsells that come with using deno. It’s an island unto itself.


Upsells?


They are pushing their SaaS pretty hard, these days.


Bun is gonna prolly end up doing the same thing though


Deno still has quirks, like not running on ARM processors.


I work on Deno, specifically the CLI. We support both Mac and Linux ARM, though the Linux ARM builds are currently produced by a third party contributor.

If you have specific issues with ARM, I'm certainly interested in fixing those. I don't have any timeline for when we'll add Linux ARM as a binary release target but I'm happy to ensure any ARM-only bugs get the proper attention.


Does that mean deploying Deno to AWS Graviton is an issue currently?

Thats the use case I have in mind to cut costs


I haven't specifically tested it but it _should_ be supported. I don't have easy access to graviton instances at this moment, but I've previously used https://github.com/LukeChannings/deno-arm64 with great success.

As long as your ARM OS is 64-bit, Deno should function properly. 32-bit _might_ be supported, but TBH we don't do any testing of those configurations as far as I'm aware.


Weird, when I run 'file deno' on my ARM Mac I get:

    deno: Mach-O 64-bit executable arm64
...also haven't experienced any problems so far.


How about in Docker on ARM Mac?


No official docker images, but lukechannings maintains ARM/Linux builds and an unofficial docker container that are well supported:

https://hub.docker.com/repository/docker/lukechannings/deno


Being using Deno for new projects for years now. Not looking back.


What are the advantages of deno over node?


Not the parent, but one very nice aspect is that an entire working Deno installation is just one file: the actual deno executable.

...when installing via homebrew on Mac you get 3 more files which look like shell-completion definitions:

    bin/deno
    etc/bash_completion.d/deno
    share/fish/vendor_completions.d/deno
    share/zsh/site-functions/_deno


Both Node.js and Bun are also single executables. Node.js doesn't (yet) transpile TS for you, and that's a single dependency away.


Yeah I was confused that the Node installation directory was showing me 18k files in several thousand directories, but these seem to be global npm installs which npm seems to put into the node installation directory (which tbh is weird, I would expect those somewhere in my user directory).


Deno is great, and I use it where I can, but a lot of front-end tooling still treats it like a second-class citizen including esbuild.


I’ve found TSX to be a better alternative to ts-node. It seems to have more sensible defaults in 2023. https://github.com/privatenumber/tsx


Love this project, used it quite a bit. However I always opt for using esno[1] instead, merely because of the name; having two tools share the same name throws me off.

[1] https://github.com/esbuild-kit/esno


> From v0.15, esno is essentially an alias of tsx

heh. It really is https://github.com/esbuild-kit/esno/blob/master/esno.js


I was about to say exactly this. If you’re writing Node scripts there’s no reason to use ts-node as tsx, by default, does the correct thing and works correctly with ES modules.

Of course, Deno offers a better experience for scripts or programs.


I tried `tsx` but it didn't work for some reason (can't remember why right now). Also, if you've been using `ts-node` and feel comfortable with it, this setup should work for you instead of switching your toolchain.


tsx v4 was released a couple weeks ago which addressed many incompatibility issues. Hope you have a chance to give it another shot :)


I definitely will!


Tsx is great but doesn’t work in all situations unfortunately. Some broken cases that I remember off the top of my head are Playwright and test coverage.


Maintainer of tsx here.

tsx should be able to handle Playwright as of v4+, and hopefully the test coverage you're referring to.

Before, it was compiling ESM syntax to CJS as an effort to ease the ecosystem's CJS -> ESM migration, and hiccuping whenever it encountered `eval()`. Now it includes smarter checks to determine if a file needs to be compiled at all and skips processing most dependencies.

Hope it works well for you!


I see v4 was published just 12 days ago. I'll have to give it another shot! Thanks!


Can confirm we use tsx with playwright with no issues. Thanks!


I use node-dev which seems to have good ts support. node-dev with --respawn is a good cheap way to watch and rerun ts files on change!


or esno, which is tsx with esbuild.


I'm using `esyes` [0] by Yehuda Katz and the logic is simple and it works very well.

[0] https://github.com/starbeamjs/esyes


thanks for sharing! Didn't know about this one.


tsx is "tsx with esbuild" though?


I just got started with a Next.js project coming from React with JS and…. another JS/TS rant incoming…..

For an ecosystem that purports to be ready for professional use it absolutely boggles my mind that the tooling is stuck at this very amateurish level.

Why isn’t there a ready preset for me to use ESM syntax with Next.js / Typescript?

What’s wrong with the culture around Node/TS/JS tooling in general that it’s so consistently broken all the time? What the hell.


Try QwikJS. It solves what you ask for, create a project and you are ready to go.

Qwik feels so right to use, if you have 5 minutes you should read about it: https://qwik.builder.io/docs/concepts/think-qwik/


This is not a guide, it's three config files


Sometimes the most valuable guide is a working example. Lord knows, there are so many docs out there that keep prattling on and on instead of showing me the code.


99% of the working examples I encounter in JS config don't work by the time I get to them.


a working example that you don't understand and just copy over and over again into new projects.

This eventually turns into an incantation and magic. It's how crap gets built up and poor decisions get propagated.


> a working example that you don't understand and just copy over and over again into new projects.

Once you have a working example, you can go through it line by line and figure out what it is doing.


Honestly, I've always preferred to have somebody's example plus the reference documentation.

Learning not only what it does but where it's documented - and having to go look for myself to do so - fixes it in my memory a lot better.

I have a years old set of npm/etc. configs in a project I noodle on occasionally, and when I re-open them to look I remember exactly what they do -because- I started from a boilerplate and then went and learned how all of it works.

That doesn't mean an explanation wouldn't still be helpful for a bunch of people who aren't me, but honestly having the tsconfig commented like that is more than sufficient for -me- to be happy.


Which the author would be kind to do and post with the example.


You're right, but what guide is easier than "Just add the following files and run npm run dev. You'll be good to go!".


I'm personally against that belief as it doesn't really teach anything. Just adds a list of bootstrapping that you don't understand.


I find that almost all devs have a layer at which they don't understand what is going on. Everything we do is built on the shoulders of giants, as evidenced by how little we think about the 0s and 1s that are the actual end result of our work.

So I don't see a problem with any dev having a line of non-understanding as they work. Some peoples lines are lower than others, but we all work that way. So it is all good.


Absolutely, but don't call it a guide. When it's not.


> how little we think about the 0s and 1s that are the actual end result of our work.

but that doesn't mean you don't understand it.

You can get away with not thinking about it - for example, the error correction algorithms and protocols in tcp, or memory access - and that's because the designers of those layers have thought hard and long about how to keep it from leaking.

For js ecosystem, the designers (?) didn't think much at all. It is hobbled together rather haphazardly over time. Leading to the mess today.


There's only so much spoon-feeding that can be done. We already have so much information at our fingertips.

If people just want to copy+paste it then mark a jira issue as complete, that's up to them.

However, for those of us who want to understand - sometimes it's better to see a working example and pick at it.


Having tried to wrangle all of this both professionally and for a side project, the link is an over-simplification. No, it won't "just work".

Part of the problem is the fact that web browsers, Node.js, and other JS runtimes have slightly different needs and expectations. Part of the problem arises when you're trying to mix and match web code, Node.js code, and other JS runtime code in the same monorepo. There's no single magic-bullet configuration.


Add eslint and jest to pull your hairs of you head. I managed to get it right last week, but I'm pretty confident that next upgrade I'll have to revise most of it.


Have you considered using Vitest? Its performance has had a significant impact on my workflow and the workflow of my colleagues. It supports ESModules by default.


Ah! thanks a lot, I'll give it a good look next round, but I won't have much difficulty moving from jest, because its old roots triggers too much unexpected and hard to anticipate constraints.


> Just add the following files and run npm run dev. You'll be good to go!

This attitude just doesn't work for js ecosystem. I have some shell scripts, vim config files, etc. that I don't remember what they do, but I just copy over and they work.

Coding projects are different; they break all the time. Especially with how fast the js/ts ecosystem works. One day, a crucial library just decides that it's changed something that doesn't work with the build process because there's so many variations of them that they can't possibly all be tested against.


I've been trying to upgrade a couple of libraries we use internally at work to output CJS + ESM, from a typescript project, and have the output be 1-to-1 files (so, no bundling / rollup / whatever you want to call it).

What a frustrating experience it has been.

Using unbuild "works", but

- For the life of me, I can't get unbuild to generate `.d.mts` files when my source files don't have `.mts` extensions. Luckily, when the library is used in a downstream project and a `.mjs` file is imported, TypeScript properly loads the `.d.ts` file anyways

- When it comes to a downstream project, TypeScript doesn't seem to work with export maps. People say it does, but maybe because I don't have `.d.mts` files TypeScript is saying it can't find type information? It _does_ work with simple export maps, but if I say `'./': './dist/esm/'` so the downstream project doesn't have to manually import from `dist/esm` I see the issue

- Using the "esm" module / moduleResolutions + converting my files to `.mts` + changing imports in the source to import the non-existant `.mjs` files results in a CJS bundle that tries to require a `.mjs` file, which unbuild has built with a `.js` extension, so it throws an error because it can't find the file.

- For some reason unbuild mucks with the hashbang line in my _CJS_ `bin` script, the ESM one it doesn't touch

Ugh.


Try tsup it does just work unless your package is doing something funny.

Then do a check with attw cli tool and it’ll report your compat mistakes with links to explainer docs.


That's what I tried using before and I had even less luck / had to do more fandangling, but thanks for the suggestion.

Like I said, these are libraries whose "build" before was just `tsc`, there's nothing funny going on.


Share a repro I’ll sort you out. Gotten good at this bullshit :D


One problem I encountered with ESM TypeScript development on the browser without bundling: many Node packages aren't set up for that.

You might ask why without bundling?

Sometimes you just want to start something simple on the browser and compile to JavaScript on the fly.

I tried the dev server from Modern Web [0], and I liked it. I program in TypeScript and the browser reloads whenever I save a file. Of course I could set up a bundler and for a small program waiting times are negligible. But I hate bundlers. I know it's irrational, but nowadays I program for fun so I think I should have the choice to reject bundlers.

This fails for many Node dependencies. There is a conflict between CommonJS and ESM. I am not 100% sure that what I want to achieve is impossible without forking dependencies and making a small change.

I even found a way to have a CommonJs and ESM polyglot, but this hack is extremely ugly. I named the hack modglot [1]. I don't think this is a good idea and I don't understand enough to propose something. I am somewhat dejected about the current state of TypeScript development for the browser and paused development.

Now I am programming in Rust again just for fun, but if I return to TypeScript, probably I will try out Deno.

[0]: https://modern-web.dev/guides/dev-server/getting-started/

[1]: https://github.com/nalply/modglot


I am pretty sure "type": "module" doesn't work with React Native/Expo and jest (in 2022 last time I tried).

If you Google the error you will get stackoverflow posts with hundreds of upvotes.

And that's the main problem with using ESM - 3rd party library support. It is trivial to do hello world in ESM with Node.js, but try that with 10 dependencies and quickly you will be hit errors.


Node.js, TypeScript (compiled script...), ESM modules, versions, and the dependencies tree hell... You guys are adopting the masochism, the problem is that you are dragging the rest of us into that hell.


I'm going through a special version of this hell right now at work. I'm updating old Lambdas that are on the node.js 12 runtime.

Some of them have internal dependencies that just _no longer exist_, or aren't compatible with Node past 14.

Meanwhile, all I had to do to update old Python Lambdas was just update the runtime itself.



Note that in my original comment, I didn't say Python doesn't have version problems. I said my (specifically my) Lambda version migration problems were much worse with Node.js than Python.

Also, I'm vaguely offended that you used a "let me Google that for you" reply as your sole comment. Did you really have nothing more intelligent to add than low-effort snark? Go back to Reddit.


A lot of people have versioning problems in all languages. You said "Meanwhile, all I had to do to update old Python Lambdas was just update the runtime itself. ". You know that isn't always true in Python that you just update the runtime and nothing goes wrong.


The reality is that JavaScript has become an ungovernable language; Python's dependency problems are nowhere near the current horror of JavaScript, where people keep using all these tools made by startups creating problems bigger than the problems such tools were supposed to solve, and those new problems are solved by a wave of new tools from more -or even the same- startups, and this in an endless loop, because people adopt it again.


At work I deal with javascript dependencies, ruby dependencies, php, and others. Dealing with keeping versions up to date sucks in every language I've used.


True, but Node.js is far worse when it comes to dependencies and _their_ dependencies.


It’s only the module resolution bullshit. Typescript is amazing, but module resolution between the browser and node is so terribly fraught it kills the whole experience.


What is the alternative? Commenting on hn and reddit?


A gist, the digital equivalent of scribblings on a stained cocktail napkin, lays bare the great mystery. The great dragon is slain! His precious treasure is of no value whatsoever.

Fellow scrounging raccoons, raise your chipped mugs and cracked cups! Let us toast to our fortune, we truly are the blessed ones.


Ah, I upgraded just yet, realized node 20 broke the way ts-node-esm loader works. Had to downgrade to 18, because there's no functionality I really need from 20. Also Vite complained when I tried node 19, because it requires <=18 && >=20 for some reason (probably good one).

I'm maintaining a few pristine example projects to keep track of these ways and test them periodically. This iteration probably had the shortest lifetime. I didn't manage to develop even a single toy project before something got obsoleted again. Love this community. Looking forward to solve a handful of brand new issues with tsx.

I wish there was some IDEish starter pack which you could install and start writing code anytime, without investigating issues with running a damn interpreter.


My guess is node 19 isn't an LTS version and it's hard enough to maintain just the LTS versions. Probably a requirement that will save a lot of pain...


Some comments and suggestions:

- `importsNotUsedAsValues` is deprecated [0] since TypeScript 5.2, in favor of `verbatimModuleSyntax` [1].

- I could set `module` to `Node16`. This automatically set `esModuleInterop` to true.

- Also, to catch more issues, set `allowUnreachableCode` to false and set `strict`, `noImplicitReturns`, `noImplicitOverride`, `noFallthroughCasesInSwitch`, `exactOptionalPropertyTypes` to true.

- Set `types` to the empty array `[]` to avoid loading unwanted types.

- Enable `skipLibCheck` to avoid checking imported module types.

- Not sure that `declarationMap` is still useful nowadays. TypeScript is now able to match directly against source files.

- Enable `composite` that in turns enables `incremental` and `declaration` (declaration file emit). `composite` enables project references which is useful in a monorepo setting or to separate source and test files into two projects. See [2]

- Enable `checkJs` to type-check JavaScript files

To summarize:

  {
    "$schema": "https://json.schemastore.org/tsconfig",
    "compilerOptions": {
      "lib": ["ES2022"],
      "module": "Node16",
      "target": "ES2022",

      "outDir": "./dist",
      "composite": true,
      "sourceMap": true,

      "types": [],

      "isolatedModules": true,
      "resolveJsonModule": true,
      "skipLibCheck": true,
      "verbatimModuleSyntax": true,

      "allowUnreachableCode": false,
      "checkJs": true,
      "exactOptionalPropertyTypes": true,
      "noFallthroughCasesInSwitch": true,
      "noImplicitOverride": true,
      "noImplicitReturns": true,
      "strict": true
    }
    "include": ["./src/**/*.ts"],
    "exclude": ["./src/specific-file.ts"]
  }
[0] https://www.typescriptlang.org/tsconfig#importsNotUsedAsValu...

[1] https://www.typescriptlang.org/tsconfig#verbatimModuleSyntax

[2] https://www.typescriptlang.org/docs/handbook/project-referen...



Strong agree. This has caused a major rift in the Node ecosystem (and for its packages).

All we can do is adapt and keep building. Using Node v20 (LTS) currently with the config linked in the parent post.


This is why I use bun now! It just works, NodeJS is such a shit show right now, feels like the python 2->3 move.


I tried to use Bun but it doesn't support some low level Node APIs properly, in my case for Playwright. Keeping an eye on it though, what I saw seeemed promising.


Make an issue. I did so for various compat issues and they are fixed now.


There are already long running issues on this topic. https://github.com/oven-sh/bun/issues/2492


I've been curious about using Bun. Could you point to an example of something like Node backend code that's written in TS using Bun?



this setup doesn’t work with node test runner.

I had to tsc then node --test the built files.

tsx also is missing the test runner https://github.com/privatenumber/tsx/issues/257


With tsx instead of ts-node, it works just fine -

    node --import=tsx --test __tests__/\*/\*.test.ts


Including failed tests?


- Why include files, exports, and types in your package.json when you are just using ts-node to transpile on the fly?

- ts-node is much slower than something like esbuild/tsx right? As long as you rely on your ide type checking or run type checking before deploying your app.


The "exports" and other sections are recommended things you'll need in your `package.json` file to support ESM. Sensible defaults. Also just points to an index.ts file (that you'll likely have in your project if you are developing a package).

Also, this project can be compiled with `tsc` or a bundler, of course.

In terms of speed, this should consistently start up an app or CLI in <3 seconds (depending on size of course).


+1 to what one of the commenters is saying. You can skip all of this and just use `tsx`. I currently sponsor that project. That's how impactful it was on all of my development.

I will say that it's kind of sad that Node.js has devolved into this.


> The experience of using Node.JS with TypeScript, ts-node, and ESM is horrible.

So true, I really wandering if there was a better alternative, they essentially broke a lot of packages and I don't know how many dev hours will be put now to fix all of this, because so many tools are broken now. Additionally to that, they did some other not backwards compatible changes, like removing __direname in ESM, which is IMHO not the best decisions, why not for example just keep it as it is, but deprecate it and have a warning message.


I agree that the migration experience has been subpar! I dropped ESM migration of my packages 2-3 times before finally picking it up again and wrapping it up in a simple guide. Hopefully with the config I shared you’ll find that you can use all those out-of-reach ESM packages now! Also, I provided examples for getting __dirname and __filename -like behavior.


> a better alternative

Deno?


Typescript is a huge improvement over JS, but I never use it. My rule of thumb is that if TS provides a tangible benefit, I have too much complexity on the frontend.


I did some experiments myself and settled on this template repository:

https://github.com/bhouston/template-typescript-monorepo

This has a lot of features that work well together: - TypeScript - ESM - Node.JS Server + React + Cli - Monorepository - Fast Incremental Updates

Feedback welcome. I use this for about a dozen different projects now.


Main reason I even tried bun out. Honestly 'just works', I've only had one issue so far and it was with a unpopular compiled module.


I love it. Had some issues with terminal stuff, now fixed.

Just works, indeed.

But I have a difficult time trying to get a project make to node/ts.


I'm really enjoying bun for my own stuff but tsx looks more promising for work projects.


Sharing https://github.com/tommyguo/node_monorepo if this helps folks. It's a Typescript monorepo template with everything you need included.

I'm currently using this setup to run multiple services on AWS.


Here comes a question what many of the people reading the comments are thinking:

As of November 2023, what is the canonical way to set up a Node project with Typescript and hot reload?

Minimal setup with least amount of configs and tooling. I am not after any other tools like Bun.


Snowpack, parcel, esbuild. Minimal setup each. YMMV based on how you plan to distribute the build results (package, site or app) after.


Maybe I'm not getting it, but I just set up an express.js app this last weekend with nothing much more than tsc (typescript) and ts-node.

Do you need all that other stuff for a simple node app?


You are getting it. Use less and smaller tools if possible. And if you're on Node 18, node comes with a watch mode built in that you can leverage.

You can alternatively use esbuild to handle the TypeScript compilation since it is faster than tsc for that, and just keep tsc around for the typechecker.


Snowpack has been unmaintained for years now, and its homepage actively recommends people switch to Vite.


I really detest this JS dx.

Ideally things are great — do ur frontend dev in js and backend in js. With TS added for better dx. Except 90% of the time ur fighting the configs of why its not importing/requiring

U change from .js, .ts, .cjs, .mjs and nothing works


And if you publush it, imports are going to be from 'pkg/dist/foo'..


I always found it easier to avoid `ts-node` as it introduced a bunch of small compat issues.

Since TS 4.5 and their support for `.mts`, I finally settled on just chaining `tsc --build && node ./build/main.mjs`.


I generally agree with your statement, but have found ts-node incredibly valuable especially when developing servers or CLIs where I want to start/restart the process many times without a build step every time.


Node has a built in watch mode and so does tsc. So if you start both you should get what you want.


or: `tsc --build --watch` and then just have another terminal to run.


Yes, TS now fully supports ESM so the watch mode works great. I'm not used to using it when working in Node, but other people I work with use it all the time.


And for development?


With TS, I get type checking in the IDE; I don't need to rebuild every few seconds.

When I actually need to build and test, it's very fast (<2 seconds). I use Yarn workspaces/subprojects and TypeScript references with incremental builds so it's not really an issue.

Here is the `package.json` [0] for a serialization lib I did, you can check the `scripts` section: it's very minimal and I'm quite happy with it. I'm an old Gulp maintainer, so for a long time I had heavy Gulp config with a lot of processing. Over the years I could get rid of it. ESM was the last thing holding me back; and now I'm so happy that I can just use a few simple commands.

[0]: https://github.com/demurgos/kryo/blob/master/packages/kryo/p...


I'm going to keep hitting reload on this page until someone solves some of the worst problems of my work life right now; getting a npm monorepo with typescript, eslint and jest working smoothly. Thank you to whoever(s) that is. I'm trying to make a public project, but building/developing with it is successive layers of sometimes-works voodoo that have nothing to do with the project goals. Somewhat compounded when members of nx (nee lerna) popped in to try to help, adding another layer of sometimes-works. As it is, DX depends on a fast computer to basically rebuild the whole thing each time, or running build & test in a dozen workspaces, because I haven't found any other reliable "workflow."


This is the current setup I'm using for my TypeScript monorepo: https://github.com/tommyguo/node_monorepo. I tried avoiding Lerna because I didn't feel like I needed it. Happy to hear feedback!


Thank you, I will take a look after current sprint.


We gave up and switched to vitest.


+1 for vitest :D I was amazed that it "just worked" after struggling for hours getting jest to work because of silly file extension problems (really... what's up with that... wouldn't that be easy to fix if the Node.js and Typescript teams would talk for 5 minutes and agree on one approach?)


I don't use Vite though. Aside from typescript, I'm trying to be as vanilla/unopinionated as possible, using Web Components for example. And there are hundreds of Jest tests I don't want to port.


You can use vitest like a standalone testing tool, it will install vite as dev-dependency but you don't need to build your whole project around vite.

In my case it was literally a drop-in-replacement for jest, done in two minutes and without a config file. A simple config file only was required a bit later when I added test- and coverage-reporting as junit.xml and cobertura format for Gitlab CI.

Didn't even have to change a single line of testing code (I didn't use Jest's mocking magic in that project though).


Just wanted to say thanks for this recommendation. I'm not finished yet, but it seems like a huge improvement over all the wtf of jest, along with a lot of new niceties.


Maybe I spoke too soon, cyclical imports problem seems pretty big.


oh yeah, and this esbuild keepNames nonsense has lost me some hair.


Thank you, it sounds pretty good, especially solving the modules problem. I will look into it when finished current sprint.


We dropped Jest and switched to Vitest across the entire stack. It's a shame what happens to these projects over time...


Could someone succinctly describe the situation between node ts and esm?

I’m not very familiar with the ecosystem and trying to understand what pain ports people are describing here in the comments.


Does Node.js plan to transpile TS out-of-the-box soon? (Picking up a legacy project, can't swap out for Deno or another runtime just yet)


This would be a nice direction for the Node community. I wish for this. Especially since so many other platforms are supporting it out of the box.


The experience of using Node.JS with TypeScript, ts-node, and ESM is horrible.

I completely disagree. This has always felt immediately straight forward to me. I suspect this struggle, a struggle I completely don’t understand, explains the complexity of hiring for these kinds of jobs. I really felt that to get hired for these jobs you had to be willing to play stupid games and abandon all reason to worship at the pulpit of giant frameworks and third party solutions. Fortunately, I have moved on to something else.


I'd love to see you live stream a process of creating a project consisting of several different apps using same ts config and shared libs folder from scratch using these tools.


In my personal projects I use ”type”: “module” in the package.json.

In my tsconfig.json I use

    {
    "compilerOptions": {
        "alwaysStrict": true,
        "module": "ES2020",
        "moduleResolution": "node",
        "outDir": "./js/lib",
        "noEmit": true,
        "noImplicitAny": true,
        "pretty": false,
        "strictFunctionTypes": true,
        "target": "ES2020",
        "types": ["node"],
        "typeRoots": ["./node_modules/@types"]
    },
    "exclude": [
        "js",
        "lib/terminal/test/storageTest/temp",
        "**/node_modules",
        "**/.*/"
    ],
    "include": [
        "**/*.ts"
    ]
    }
Internally in your apps never never use relative file system paths.


Is alwaysStrict the same as strict? I think this skips a lot of the strict options that are usually the case for using TypeScript in the first place.


According to the TypeScript documentation for tsconfig.json the alwaysStrict option forces the compiled out to strict mode such that the JIT interpreter parses it as such. This happens anyways when using ES6 modules, but it also ensures the TypeScript compiler parses each file in strict mode regardless of having the "use strict" pragma at the top.

https://www.typescriptlang.org/tsconfig#alwaysStrict


I just dug a bit deeper as I couldn't remember the differences. They're not referring to the same things.

"alwaysStrict", which adds "use strict", is different than TypeScript "strict" mode, which constrains the language.

Setting "strict" to "true" enables "alwaysStrict", but not the other way around. I'd remove "alwaysStrict" and go with "strict" as it covers more things.


In order to be a guide, I feel like it would need to explain _why_ each thing is there and what it does.


I thought there were "presets" that NPM and Yarn knew about to do this kind of thing...


Hear me out: if you need to do this just to use a tool effectively, maybe it's a bad tool.


We're open to suggestions!


In this specific context: just use plain JavaScript with default args and your own, inline type checker functions. No need for stuff like this and you can customize type checks to your heart's content.


"...for a few months."

Until this github repo dries up the same way similar attempts at this have.


Why not using Node how it is designed, than ESM works great.

For type checking on development, you can do it with ESLint and JSDoc in Typescript modus. You have the same type checking like you have in ts files. You can even import types from typescript files, like .d.ts

Best of both worlds, no transformation of the code, and on development you have some help from Typescript.


Module resolution "bundler".

ts-node --esm --swc

Haven't really had any issues with this in a long time.


Now add eslint and prettier :)


Agree it is a little tricky to get startet. But once configured, it helps a lot




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: