This gist is the setup and workflow I use to develop TypeScript on node.js. This readme justifies the choices made here. Feel free to recommend better choices.
npm install
npm build # or `npm build:watch` or `Ctrl+Shift+B`
npm test
- Mark the corresponding
describe
andit
with.only
(see node:test docs). - Run the debugger (F5)
Note: the included VS Code launch profile runs node with --test-only
so it will only execute tests that are marked as only
. Unlike some other frameworks, node:test
requires that you mark the whole ancestor chain with only
(i.e. all the nested describe
blocks).
- Open project
- Run continuous-build task
- Write code
- Write tests
- Run tests
- Mark failing test with
.only
(and its ancestordescribe
blocks). - Add relevent breakpoints.
- Run debug launch configuration in VS Code.
- Step through the code.
- Notice an unexpected state. Look for source of the unexpected state:
- Set breakpoints earlier in the program, where the offending state should have been created.
- Restart the debugger (
Ctrl+Shift+F5
). Go to step 8. - Fix the source of the problem.
- Go to step 4.
Because they're builtin, so it's fewer dependencies and I expect it to get the best support going forwards.
I've found that when I'm debugging a test failure, I'll rerun the failing test many times before getting to the bottom of the issue. Test cases can be run in VS Code using the relevant VS Code test extension, which for node:test
is the extension node:test runner
.
But for me it's not acceptable to run a test each time by:
- Navigating to the test file (which you're likely not in, because you're debugging one of your source files).
- Scrolling to the top of the test (if it's off screen). If there's multiple tests then you have to make sure you're in the right one.
- Moving the mouse to the little icon next to the top of the test and right clicking it.
- Moving the mouse to the "debug" button and clicking it.
Contrast this with just pressing F5
and the test will run again no matter which code file you're in.
Marking a test as .only
is a quick change at the beginning of your debug process that leads to a much faster debug loop time.
Another reason to use a launch configuration to debug tests rather than specifically the node:test runner
VS Code extension, is that I like to debug tests with "Caught Exceptions" breakpoints enabled. This is a huge productivity boost because in the case where your problem is visible as a thrown exception, you'll jump straight to the point where it's thrown and can immediately start inspecting the state that caused the exception. But this is only a reasonable approach if you don't throw exceptions as a normal course of events. Unfortunately, at least on my machine at the time of this writing, the node:test runner
extension throws an exception as part of its normal startup process, which to me makes it incompatible with the enabling "Caught Exceptions" breakpoints.
The npm build:watch
task (or default build task in VS Code, with ctrl + shift + B
, thanks to .vscode/tasks.json
) will continuously build the project in the background.
This is to improve the rate of feedback during dev:
- Continuous build as a background task immediately shows errors across the whole project when you make changes.
- Continuous build is much faster than manually building as a discrete step with
npm build
. npm test
is super fast because it doesn't need to compile TypeScript files at all.- Reduced delay between starting the debugger and actually debugging your code.
- Since the debugger is running JavaScript, you won't see a loader (like tsx) in your call stack.
The combination of this and the "Node16" module configuration in tsconfig.json
means that TSC is outputting clean ESM module code. This is again mainly to improve the developer experience.
When stepping into a function in another module, if there is generated scaffolding CommonJS code then often you step into that first (the debugger jumps to the top of the file) before stepping through to your actual code. By having the output be ESM, the debug stepping experience is cleaner. This applies to other kinds of scaffolding code as well which subtly slows the debug experience.
The problem is that npm install
only happens once, but for a dev cycle you need to keep building every time something changes.
Incorporating npm build
as a pre-step into npm test
means that the tests are guaranteed to be testing the latest TypeScript code, but it comes at a significant cost of performance since then npm test
implicitly must build the whole project. This slows down the development-debug cycle because it takes longer to test.
Another option is to have a loader, such as node --import tsx ...
to compile the TypeScript on the fly. This has the advantage that you don't need to explicitly build, but the disadvantage that it's again much slower to run the tests because they need to be compiled each time.