Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Did anyone make a large TypeScript codebase work with ESM + TS + node (together with IDE support and yarn pnp support?)

The thing that is annoying with ESM is that it requires to have extensions in imports, i.e. `import .. from '/foo.js'`.

This gets messy with TypeScript, where your files are named `foo.ts` but you need to import `foo.js`.

The previous "best practice" in TS world was to have extensionless JS imports, so this move would require a massive codemod to update all imports in an entire codebase.

For now, we've been using `ts-node` with swc under the hood, but without ESM. I tried `tsx`, but the compilation time of esbuild is way too slow, some of our node CLI tools written in TS take 15s to boot up, which is not acceptable (with `ts-node` it's 3-4s) (tbh, it's probably partly a fault of our CLI framework, which crawls all workspaces to discover all CLI tools, and as we have a lot of them, tsx has a lot of useless work to do).




Hey! Not sure how modern your codebase is, but you can consider the following tsconfig settings:

- rewriteRelativeImportExtensions: this will allow you to write `import foo from './foo.ts'` and have tsc transform it to `import foo from './foo.js'`

- erasableSyntaxOnly: this will error on non "erasable" syntax, that is, TypeScript code that has a runtime output (e.g. enums)

With these two settings enabled, you'd be able to run TypeScript code directly with Node: `node src/index.ts`, and cut boot up time substantially


To add, those are the recommended tsconfig.json settings in Node's docs on native TS stripping. Here are the rest: https://nodejs.org/api/typescript.html#type-stripping


While `rewriteRelativeImportExtensions` works for frontend applications, it has a significant limitation: it doesn't fix declaration files (.d.ts), which is problematic when developing libraries or public packages.

I created a small tool to address ESM + TypeScript issues that the tsc doesn't handle: https://github.com/2BAD/tsfix


Also there are eslint rules older than erasableSyntaxOnly that can also be useful in doing a "rip-the-bandaid-off-refactor" using all the lint warnings/warnings-as-errors to add extensions everywhere in the case where your brownfield also needs to be a version or two behind on Typescript.


How about for aliases?

import foo from '@Schemas/foo.ts' won't work since it is not a 'RelativeImport'. Is there a fix for this use case?



It’s pretty easy to find/replace everywhere that needs it.

In my experience the reason people don’t is that it offends their aesthetics.

Which I understand. But personally I don’t program for aesthetics.




Consider applying for YC's Fall 2025 batch! Applications are open till Aug 4

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

Search: