TypeScript projects in Visual Studio (Look ma, nolib)

I recently tried to set up a TypeScript project in Visual Studio which makes use of a WebWorker, and found the experience to be generally confusing. Web-workers don’t have access to the usual browser API (e.g. they cannot access the DOM and will find different window globals to be available), and there are a few worker-specific APIs available. For this reason, there is a different version of the TypeScript standard library definition file available, lib.webworker.d.ts (which depends on a common lib.core.d.ts). Since the usual lib.d.ts is included by the TypeScript compiler by default, the command line tsc has a --nolib option available to disable this automatic inclusion, so you can start from scratch and use your own definitions (ie the WebWorker one).

However, in Visual Studio this option is not available in the UI (even in 2015). This set me down the rabbit-hole of the different ways of configuring TypeScript in Visual Studio. Note that I’m really only talking about the IntelliSense experience, there are also different ways of configuring compilation (e.g. visual studio itself, via task runner). As we’ll see, things are at least getting better with support for the standard tsconfig.json configuration file.

The points discussed below also apply to various other scenarios, such as unit tests (which are not part of your main deployed project), and other compiler configuration options in general.

History and Compilation Contexts

First, a little history. Originally in Visual Studio, if a TypeScript file referenced another, you had to add a “reference comment” of the same kind that is used to light up IntelliSense for plain JavaScript files:

/// <reference path="file/path/definitions.d.ts" />
/// <reference path="file/path/code.ts" />

As far as I recall, you needed to do this in each file for each referenced TypeScript code or definitions file. What you could also do is reference one project-wide references file, which then referenced everything in your project - and in fact Visual Studio provided some built-in support for this “_references.ts” file.

In VS2013, Visual Studio switched to use an implicit compilation context - all files in a project are compiled together in the same context, implicitly referencing each other. The above reference syntax can still be used to impose compilation ordering and reference external files into this context. Similarly for files in a “web site” and files opened outside of a solution, there is (usually) a single compilation context. You may have seen Visual Studio display a virtual project when opening such a “loose” TypeScript file:

Virtual Project

There is an option to always show such virtual projects, which will be useful to understand the compilation contexts in this post.

Virtual Project Setup

The Problem

I’ve set up a very simple solution with 2 TypeScript files, one (main.ts) to run in the standard browser context:

var worker = new Worker("worker.js");
worker.onmessage = ev => {
    window.alert(ev.data);
};
worker.postMessage("Hello world");

And the other (worker.ts) to run as a WebWorker:

self.onmessage = args => {
    self.postMessage("You said: " + args.data);
};

If we simply add these to a web project in Visual Studio, due to the implicit reference of lib.d.ts, the main file is fine, but the worker displays incorrect IntelliSense errors:

Invalid Error in WebWorker

In this case, postMessage exists both in browser and worker contexts, but with different signatures. Conversely, IntelliSense leads us to think that APIs are available which are not:

Invalid Success in WebWorker

We can also see that all files are (as expected) compiled together, so that if we fixed these issues for the worker case, the main file would be in the wrong context:

Mixed Files - Virtual Projects

To avoid this, we’ll set up two separate folders, one for “standard browser” code and one for the WebWorker code, and ensure they both run in a separate context.

Some further information on references, virtual projects and some other issues discussed here..

1. Web project with NoLib setting

We start with a simple solution. With 2 standard web projects, we immediately see the code is now in 2 separate compilation contexts, but with the wrong library included for the worker:

Basic virtual projects

We would then like to edit the TypeScript configuration on the worker project to enable the --nolib compiler flag. There are a number of options in the UI, but this is not one of them; despite this, the underlying machinery supports the flag. We can add the following option by editing the .csproj file directly:

    <TypeScriptNoLib>true</TypeScriptNoLib>

A number of other options are available, some of which also lack UI configuration (note that these have changed since previous versions). With this we see that the correct lib file is referenced and IntelliSense lights up correctly:

Virtual projects with nolib Correct tooltip

2. No project with tsconfig.json

There are a couple of ways of ending up with “loose” TypeScript files not in a project:

  • Opening files outside of a Solution (directly)
  • By creating a solution and adding a “web site” (as opposed to a web project).

In either case all TypeScript files by default form a single compilation context. Or rather, all files that you open - I can see some files in the Solution explorer in my web site which are not opened, and so don’t form part of the compilation context. To ensure that files are included in the context, we can use reference lines as above. In fact this even works when referencing the lib.webworker.d.ts, as this suppresses use of lib.d.ts through a

/// <reference no-default-lib="true"/>

Virtual projects with no project

However this will not give our browser and web-worker files different compilation contexts, even though they are separated by folder structure.

Introducing tsconfig.json

Starting with TypeScript 1.5, a configuration file called tsconfig.json is supported by the TypeScript compiler. This allows setting compiler options and specifying files to include (defaulting to all files in the directory/subdirectories). This config file is also supported in the VS2015 IDE (somewhat!) as well as editor plugins such as Atom/Sublime.

For our purpose we can drop a separate tsconfig.json in both the browser and web-worker folders, and we get separate virtual projects with different configuration. For the WebWorker project, it’s as simple as:

{
  "compilerOptions": {
    "noLib": true
  }
}

And we see the desired result of creating separate contexts for each folder:

Loose tsconfig virtual projects

Unfortunately this can still be a painful experience, as we don’t have compilation on F5, Visual Studio will happily show you IntelliSense errors but run your site anyway, and files are only compiled on save (if you have that enabled). However, if you intend to use an external task runner such as Grunt or Gulp to compile TypeScript, perhaps as part of a build shared with other non Visual Studio users, this could still be a viable option—particularly when you consider hooking this in with the new Task Runner to do this automatically on build.

3. TypeScriptEnabled=false

Unfortunately, tsconfig.json is not currently supported for standard web projects. As an amusing (and painful) diversion, we see how to enable it.

By adding the line

    <TypeScriptEnabled>false</TypeScriptEnabled>

to our project (in a <PropertyGroup> at the end of the project file, after the Microsoft.TypeScript.targets are imported) we in a sense jump back to the old world of reference comments described above. More accurately we are back in the same situation of loose files. Again, we can use reference comments to bring files into the compilation context, or even disable the implicit lib.d.ts reference. More to the point, we can make use of tsconfig.json to create multiple compilation contexts, and set appropriate compiler options.

The same comments around an annoying lack of IDE support for actually building the project apply as above.

4. ASP5 projects - real tsconfig.json support

With ASP.NET 5, the nature of a project changes dramatically with the .xproj project format, for example configuration largely in project.json. Microsoft is now encouraging the use of npm and Bower for dependency management, Grunt or Gulp for task management, resulting in projects that play better with use of alternative IDEs and tooling. In accordance with that, for TypeScript there is good support for tsconfig.json. With a single .xproj project, we can include tsconfig.json files in multiple subfolders:

ASP.NET 5 solution

And get separate TypeScript compilation contexts with the appropriate settings, correct IntelliSense etc:

ASP.NET 5 virtual projets

This really is so much better, even if you don’t intend to run an ASP.NET 5 site. I just wish support like this was available for a “plain client-side web project” of some sort that doesn’t require any ASP.NET ‘overhead’.

5. Node projects

Having surveyed the options for a project involving WebWorkers, what about TypeScript targeting Node.js? A definitions file is available for Node, giving access to all the standard APIs, but when we consider APIs available in the browser but not under Node, we have the same lib.d.ts issue:

Node fail This should not typecheck

We see that window.alert shows up in IntelliSense even though it will of course not work under Node. So again we could look to use an alternative base definition library (one would have thought lib.core.d.ts) in addition to the Node definitions. However, it seems that the currently popular Node definitions rely on the standard lib.d.ts. The TypeScript team seem to be looking to split up the standard definitions in the future, so hopefully this will improve and we can see a set of Node definitions based on a suitable library that does not bring in extra browser ‘junk’.

If you do want to develop for Node in Visual Studio, the Node.js Tools for Visual Studio (NTVS) seems to be the obvious option. NTVS now has good support for TypeScript, and comes with its own .njsproj project format.

Unfortunately, this does not currently support tsconfig.json (support is planned), instead it supports the same TypeScript configuration page that standard .csproj web projects have. Therefore there are again a couple of (poor) temporary options:

  • Edit the .njsproj file to add <TypeScriptNoLib>true</TypeScriptNoLib> (or whatever option you want to change with no UI)
  • Edit the .njsproj file to add <TypeScriptEnabled>false</TypeScriptEnabled> (amusingly there will also be <EnableTypeScript>true</EnableTypeScript>) and deal with the pain of no longer having proper project builds etc. as above

Conclusion

I have spent some considerable time going on about how to enable a simple compiler option in a multitude of different ways, but really this has been about the variety of ways of dealing with TypeScript projects in Visual Studio. In particular an understanding of the TypeScript compilation context(s) is applicable to projects with client/server parts, as well as to test projects which may bring in test dependencies not available in the main application.

At the moment if you’re in a place to use VS2015 I’d definitely recommend moving over to use tsconfig.json, and ideally an ASP.NET 5 project due to the better general experience, particularly if your development team also includes people using other IDEs/editors. In a VS-only ecosystem it might make sense to wait until support is better across all project types, but this seems to be the way that TypeScript is developing for the future.

MORE BY NICHOLAS

CodeMesh 2017

TypeScript compiler APIs revisited

blog comments powered by Disqus