What’s happening with TypeScript typings

jakeherringbone

Alex 🦅 Eagle

Posted on November 17, 2020

What’s happening with TypeScript typings

I work on the Angular 2 team, which is a fantastic chance to make some big improvements in developer productivity (or happiness, just as well). I’ve been in this field for 6 years now and I’ve started to see some patterns. One of them is, many developers start out their career with an aversion to changing or adapting their workflow.

This is true for editors and IDEs, and developer tools in general. Beginners are a little lost among the options, and rather than increase that feeling of discomfort you already have about your lack of experience relative to your peers, you stick with something you know. It’s whatever editor you used in your CS classes, perhaps, which you started using because it was the one your teaching assistant showed you, or the one that was convenient to access on your college network. I’ve never met someone who started out by trying every editor for a week, then picking the one that was most ergonomic for them.

Really, you should re-evaluate your toolset all the time. How can you make yourself more productive? There is such a wide range of techniques out there. Hack your brain. Meditation. Read a technical book. Get a l33t keyboard. And yes, maybe try another editor. Maybe that editor can do something to increase your productivity. I’ve seen developers gain more experience, and use their self-confidence to take the short-term hit of not knowing where any of the buttons and dials are anymore. Because they know that over the hump, there is possibly a big payoff over several years.

I’ll get on topic, finally. I think the biggest productivity feature in your editor is its ability to understand the code you’re writing and help you get it correct the first time, and later to make safe changes so maintenance work stays the minority of your time. And editors can only understand code if you make the code machine-readable. That means not putting documentation in comments, or test cases like in an untyped language. The editor needs you to tell it the types so that it can be a co-pilot.

Was I about to get on-topic? TypeScript! A few of us on the Angular team focus almost entirely on using the language tools to power smart stuff. It turns out, when you build something directly into the compiler, you have the perfect environment to understand the code perfectly, and do something other than produce the executable output.

TypeScript is only as smart as the types you assign (or it can infer) in your code. When you use a library, things get a lot trickier. We need to discover the types in the APIs you’re using. In other languages that were typed from the beginning, like Java, the type information always accompanies the compiled code. But for TypeScript, which is just a superset of JavaScript, there is nowhere for the type information to go in the executable form of the code. JavaScript has no type syntax, and even something like JSDoc annotations doesn’t work in general because the code is so de-sugared (eg. turning classes into complex IIFEs) that information about where the type lived is lost. We really need a foolproof way for the types of the library to be available whenever that library shows up to the TypeScript compiler, without making developers chase down the type information and re-attach it themselves. Sadly, this is not the case today! Let’s fix it!

There are a few cases which have different prognoses.

The easiest case is when the library is authored in TypeScript, as you’d expect. The compiler produces “header” files, ending with .d.ts, which are included alongside the .js executable code. Now in your program, you import {} from ‘library’. TypeScript understands a few ways to interpret where the ‘library’ may be found on disk; we even customize this in some things like our custom builder (included in angular-cli).

If the library is not written in TypeScript, but the maintainers want to support TypeScript clients, then they could hand-write a .d.ts file and ship it along with the library, so the client can’t tell the difference between authoring languages. In practice, I have not seen this approach taken a single time. Including something in your distro means taking responsibility for its bugs, and it’s pretty hard to write automated tests to ensure that the TypeScript typings you ship match your sources. Maybe we can write some more tooling to support this.

The vast majority case is that the library is not written in TypeScript. I hope we can improve this situation by providing library owners with a pull request that gives them the typings, the distribution semantics, and also a README.md to help them maintain the typings. Most importantly, we have to give them a means to automatically determine if the .d.ts content is still correct as they make changes to the library. For example, we could try to type-check all their examples using the .d.ts file.

There will always be the case when the library maintainers don’t want to own the typings (or there are no maintainers to be found). For libraries which target nodejs, you can be sure they have some commonjs-format exported symbol, and this can convieniently be attached to typings. But a lot of libraries only have the side effect of sticking some symbol onto the window object when they are loaded. These can only be typed by sticking the typings into a global namespace as well, and just as global namespace pollution is bad at runtime (is $ the one from jQuery or Protractor?), it is bad at type-check time. These global typings are typically called “ambient”. Ambient typings work by declaring global variables, or “namespaces” which is a TypeScript term for some object that just contains some properties. You can tell something is ambient if there is no ES6 import statement that causes the symbols to be visible in your source file.

A perfect example is the type of Promise. This is an ES6 API, so when you are compiling to target ES5, the compiler rightly gives you a type-check error that the symbol doesn’t exist, because it won’t at runtime either. However, you might be using a browser that supports the Promise API in ES6, or you might be using a shim like corejs that implements it for you. Now you could tell the compiler to target ES6, but maybe there are other APIs that are not implemented in the target browser. Really your target is now ES5+es6-promise. To make the type-checker see this, you just add an ambient typing for es6-promise into the compilation unit (by a /// anywhere in your code, or to avoid brittle relative paths, by handing the file as an explicit compiler input). How do you get this typing on your machine so you can hand it to the compiler? What’s the correct version? Well, the TypeScript team is already working on that. By splitting the stdlib file for ES6 (called lib.es6.d.ts) into many small files, one per feature, you’ll be able to effectively target ES5+es6-promise with only the stuff shipped with the language. Note that this solution for ambient typings only works for standardized APIs (like es7-reflect-metadata) where you could choose any conforming implementation at runtime.

Ambient typings for non-standard libraries are harder. The compiler won’t ship with types for all libraries in the world, so we’ll have to fetch them from somewhere. One design the team is considering is, can we have a parallel distribution mechanism for types, such as an npm scoped package. Now the registry where you resolve the package, as well as the version of the runtime, could be translated simply into a corresponding registry location for the compatible typings. And, we can follow the dependency tree, so you have types installed for the transitive closure of dependencies. There’s a wrinkle here, which is that the library won’t release a new version when you make bugfixes to the typings, so you need a way to say “you have version 1.2.3 of the typings for library@1.2.3, but we now have a newer version 1.2.3 of the typings”. So some npm changes would be needed, making this a big effort.

I mentioned the problem of the global namespace of ambient typings, which is ripe for collision. The other kind of typings are called “external modules” which are much better (confusingly, there are no longer “internal modules”, these became namespaces). You can tell something is an external module if there is an ES6 import statement that brings it into scope. This gives you a location to rename the symbols, so you can use the “util” object provided by libraryA in the same file where you use the “util” object provided by libraryB, using something like “import {util as utilB} from ‘libraryB’”.

In the http://github.com/typings project, @blakeembrey has done an interesting trick of fetching typings which were defined as Ambient, and making an external module out of them. This encapsulates the otherwise global pollution, and works as long as the library provides some export.

Long term, @blakeembrey and the TypeScript team, as well as the Angular team, are all collaborating to find a mechanism for most users to have the type-checker “just work” for most libraries. It’s a tough problem but a lot of fun to be involved to help solve it.

💖 💪 🙅 🚩
jakeherringbone
Alex 🦅 Eagle

Posted on November 17, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related