TypeScript is an amazing language that helps developers be more productive when integrating new libraries, or avoiding mistakes in their own codebase. Modern IDEs come with built-in tooling for the language. However, things can get tricky when a more elaborate setup is required.
In this post we will configure TypeScript compiler to give us live feedback on type errors even when running in parallel with other tools in the terminal.
Setup
For this example project we are going to use Vite build tool that has started to gain popularity recently. Vite supports Typescript out-of-the-box, but only for transpiling, and not for type-checking that we want. For a package manager we will use Yarn, but any other would work as well.
Keep in mind that running commands from this post will install the latest versions of tools and libraries, which may be different at the time of reading.
We will use the create
feature of Yarn to quickly set up a new Vite project.
When prompted with the dialog, choose vanilla
option and then vanilla-ts
template.
Note, this command will install a global Yarn package create-vite
.
As a result we should get a simple project structure:
.
├── favicon.svg
├── index.html
├── package.json
├── src
│ ├── main.ts
│ ├── style.css
│ └── vite-env.d.ts
└── tsconfig.json
After that we can follow commands suggested by create-vite
to install dependencies and start a dev server.
If all commands succeed, we should be able to navigate to the localhost to see the rendered version of our web page.
Moreover, if we change and save any files in our src
directory, the page will be automatically reloaded with fresh content.
Making errors
Let’s modify the main.ts
file to make it even simpler than the template:
import "./style.css";
const app = document.querySelector<HTMLDivElement>("#app")!;
app.innerHtml = `<h1>Luck-typing!</h1>`;
Note that the app
variable has a type HTMLDivElement
.
If we make a mistake by accessing a wrong property innerHtml
of the app
, we will see the error in the editor.
However, Vite would happily reload the page without showing any errors, but the page would be empty.
This can be problematic if the change affects multiple components in other files.
We will not always know about the errors until we open those files or run a full build.
If there are hundreds of files in the project it will not be feasible to check each of them on every change. The only thing we can do now to be sure that nothing was broken is to run the full build.
Aha! Now we know where the error is. But can we avoid building the whole project just to see if some files were affected by our change?
Trusting libraries
Depending on the setup TypeScript can complain about the types of external artifacts, such as @babel/node
.
One way to deal with this is to pass --skipLibCheck
flag to the tsc
when running a command that uses it.
However, it is better to avoid adding extra flags if there are no errors in the first place.
Reducing emissions
Depending on the setup Typescript compiler can start creating .js
files right next to .ts
files.
Since we are trying to only check types and not compile code, we can disable this behavior.
The simplest option is to add noEmit
field to the TypeScript config:
{
"compilerOptions": {
"noEmit": true
}
}
Another option is to pass --noEmit
flag to the compiler.
Watching types
Humans are not adept at performing hundreds of routine tasks such as clicking on every file after every code change. Computers do this job much better, but only if we found a way to explain exactly what they need to do.
Luckily for us, TypeScript compiler includes just the right tool — a file watcher.
We can activate it by running the compiler with the --watch
flag.
Instead of doing a one-pass check of the types, showing the errors and exiting, the watcher keeps running until stopped.
Whenever a file on disk is changed, the watcher re-runs the type check and prints the new error messages.
Similarly to the build
command, the output of the watcher is colored, which helps to identify errors visually.
Let’s create a new Yarn command in package.json
to make it easier to extend it later.
{
"scripts": {
"watch-typecheck": "tsc --watch"
}
}
Nice! Now we can watch types just by running yarn watch-typecheck
.
Watching types with friends
The fact that Vite does not require the types to be correct reloading is actually a feature and not a bug. It gives the developers an option to check the types instead of breaking the whole UI because of a small error that is irrelevant for a task at hand.
However, sometimes it is good to know what effects a refactoring has on the rest of the codebase. How can we get this feedback without losing the comfort of the hot reloading? The answer is to run both tools at the same time!
There are many ways
to run Yarn commands in parallel.
In this project we will use a package called concurrently
, but any other will do as well.
We can add it to the project by running yarn add -D concurrently
.
Now let’s change the dev
command to do type checking together with hot reloading.
{
"scripts": {
"dev": "concurrently --kill-others \"yarn watch-typecheck\" \"vite\""
}
}
This command will run yarn watch-typecheck
in parallel to vite
.
In case one of them fails --kill-others
flag makes sure to stop the other and exit.
Great job! Now we have a nice setup to comfortably do hot reloading and see type errors at the same time.
Reconciling output
Although this setup works, there are a couple of problems with it.
When we save changes in any of the source files.
- Output provided by the dev server (or any other tool running in parallel) is overwritten by the TypeScript even if there are no errors.
- Errors in the terminal are not colored anymore, and it is much harder to make out their structure.
Output
First, let’s deal with the overwritten output.
The TypeScript compiler assumes that it is the only program running in the terminal. Whenever the source files change, the type errors may change as well. This means that the previous errors may no longer exist or have a different reason to be shown. That is why the compiler would clear the contents of the terminal completely in order to avoid confusing the developer.
However, in our setup this is a problem since other tools running in parallel may produce valuable output that must not be lost.
In order to fix this we can pass --preserveWatchOutput
flag to the compiler to completely turn off terminal clearing.
Colors
Now, let’s make errors colorful again, so they are easier to spot in the output.
We have already seen that when running watch-typecheck
command without concurrently
the output was colored.
This is the default behavior when TypeScript compiler knows the output goes directly to the terminal.
However, when we wrap this command with concurrently
,
the compiler starts to think that the destination for the output may be a regular file or even another program.
The compiler disables the output coloring in order to avoid weird errors with terminal control characters downstream.
Since we know that the output will end up in the terminal anyway, we can force the compiler to preserve the colors.
This is done with the --pretty
flag.
Final result
Now we need to update our watch-typecheck
command to incorporate these changes.
This is how the commands look after we include all the modifications:
{
"scripts": {
"dev": "concurrently --kill-others \"yarn watch-typecheck\" \"vite\"",
"watch-typecheck": "tsc --watch --pretty --preserveWatchOutput"
}
}
We can run the yarn dev
command again to make sure that both tools happily write colorful messages to the terminal.
Conclusion
In this post we learned how to combine live project type-checking with any other tool running together in the terminal. We also added some tweaks to preserve output of both tools and keep things colorful.