Debugging Typescript in Neovim
Table of Contents
If you prefer a video version of this topic, it can be found here.
Most of my career as a programmer, I took debugging for granted - all I needed was to press a button or set up some basic configuration and that’s it! At worst, I had to try to understand what I was configuring, but I would confidently say that I have always succeeded in debugging any project I set my mind to (my biggest achievement in this regard was debugging Docker’s engine).
In this article I will share a simple (almost obvious) debugging concept that can be used to debug any program. This time, we will focus on TypeScript and we will see how to debug any TypeScript program both in Neovim. Let’s get started!
My Neovim debugging set-up
You can find my dotfiles here.
As a prerequisite, make sure you have installed js-debug-adapter
. The easiest way to do so is via Mason.
Then, apart from the well-known mfussenegger/nvim-dap
and rcarriga/nvim-dap-ui
plugins, there is really nothing special as far as required plugins are concerned. The part where we need to pay a little more attention to is the actual configuration.
First, we need to set up the debug adapter for node:
dap.adapters = {
["pwa-node"] = {
type = "server",
port = "${port}",
executable = {
command = "js-debug-adapter",
args = {
"${port}",
},
},
},
...
}
A quick clarification:
${port}
means that the port for the debugging session will be resolved automatically.
Then, we can define some basic debug configuration for TypeScript:
dap.configurations["typescript"] = {
{
type = "pwa-node",
request = "launch",
name = "Launch file",
program = "${file}",
cwd = "${workspaceFolder}",
},
{
type = "pwa-node",
request = "attach",
name = "Attach to process ID",
processId = utils.pick_process,
cwd = "${workspaceFolder}",
},
}
There are other things we could define and set up (such as keymaps, visual elements, etc.), but, for the scope of this article, what has been presented so far is sufficient. The entire debug configuration can be found here. Let’s put what we’ve been configuring so far to practice!
Debugging simple files
Consider this simple TypeScript file:
// foo.ts
function foo() {
console.log("hey!");
}
foo();
All we have to do is to:
- place a breakpoint using
:DapToggleBreakpoint
, let’s say atfoo()
line - start a debugging session with
:DapContinue
- choose Launch file
Then, we should see something like this:
except the lint error.
So… it works! However, this is merely the simplest example. In reality, we are dealing with much more complex projects.
In fact, the current configuration does not work with even the simplest TypeScript project - i.e. a tsconfig.json
file and a TypeScript file.
Assuming we have a main.ts
file:
function foo(): string {
return "Andrei";
}
console.log(foo());
and a basic tsconfig.json
:
{
"compilerOptions": {
"sourceMap": true
},
"include": ["./main.ts"]
}
then, following the same steps as above, we will notice that debugging does not work:
At first, it may seem unfortunate - in VS Code this works as a breeze! However, we can see this problem from a different perspective - it’s an opportunity to learn something new. In the next section, we will see a simple, yet powerful command that will open so many doors when it comes to debugging.
Debugging any TypeScript project
The way to solve this problem is to leverage other ways of starting the execution of a Node program.
After building our little project with tsc
, we can successfully run node ./lib/main.js
and 'Andrei'
will be printed to the console.
Note: ensure source maps are enabled in
tsconfig.json
.
The good news is that Node provides the means to start a program in debug mode. In Debugging Node.js, we can see that we can use the --inspect-brk
option:
➜ ts-playground node --inspect-brk ./lib/main.js
Debugger listening on ws://127.0.0.1:9229/16340a2b-7f5c-4bbe-9865-eb361c478b1b
For help, see: https://nodejs.org/en/docs/inspector
As you have noticed, 'Andrei'
has not been printed to the console, which means the program has not actually run. This is expected because --inspect-brk
will ensure the program will start in debug mode and will break at the beginning of its execution.
Then, all we have left to do is to debug the main.ts
file from Neovim. After placing a breakpoint:
- run
:DapContinue
- select Attach to process
- find the process that has started due to
node --inspect-brk
- debugging now works! (if you don’t notice any visual feedback after selecting the process, try any debug command, e.g. step into)
This entire process, along with other details, can be found in the video I made about debugging TypeScript in Neovim.
If you don’t want the program to stop at the beginning of its execution - for instance, you might want to debug a running server that other programs depend on and any delays to its initialization will result in connection failures - you can simply use --inspect
instead of --inspect-brk
.
One such instance where
--inspect
makes the difference can be found at Debugging TypeScript Language Server in Neovim - in the video, we are debugging the TypeScript Language Server that Neovim uses to achieve what’s known as IntelliSense. And since Neovim LSP’s client needs to establish a connection to the Language Server as soon as possible, breaking the program at its execution will result in not being able to debug the server due to initialization errors (it took me a few hours to realise this - but it was worth it!).
Lastly, it is worth mentioning that there is an alternative debug client: Chrome’s Dev Tools. After the debug server has started, head over to chrome://inspect and select the appropriate process.
Conclusion
From a philosophical standpoint, in this article we have learned that an obstacle could be an opportunity to expand our knowledge. Technically speaking, we have seen how to debug basically any program that we can build ourselves - we can simply start it with a debug server attached to it and then attach debug clients to it!
I hope you have found this useful. Thank you for reading!