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.

yarn create vite

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.

yarn yarn dev

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:

main.ts
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.

yarn build yarn run v1.22.18 tsc && vite build src/main.ts:5:5 - error TS2551: Property 'innerHtml' does not exist on type 'HTMLDivElement'. Did you mean 'innerHTML'?

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:

tsconfig.json
{
  "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.

yarn tsc --watch

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.

package.json
{
  "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.

package.json
{
  "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:

package.json
{
  "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.