Using TypeScript's new "unknown" type to safely handle global 3rd party libraries

TypeScript advertises itself as a typed superset of JavaScript. For me this is the main reason for using it. However when using a globally available third party library from within your neatly typed TypeScript code you usually have to trust a type definition file. TypeScript uses it to reason about the external code. But sometimes the assumptions which are baked into such a file are just too optimistic. Luckily TypeScript's new "unknown" type can be used to enforce a runtime check of 3rd party code. This is useful because trust is good, but verification is better.

Last week I worked on a website which is meant to promote the release of the White Album by the Beatles in Germany. But that doesn't mean that we are 50 years back in time here in Germany. The new album features previously unreleased recordings and is meant to celebrate the 50th anniversary of the original album. As part of the campaign a real London-style cab is driving around various cities in Germany. Its passengers can enjoy a free cab ride while listening to the new album. To allow people to get an idea of where they could catch the cab its current position gets displayed on a map on the website.

We used HERE Maps for embedding the map. For some reason maps (like analytics and possibly other types of services as well) still need to be embedded by adding a couple of <script/> and <style/> tags. No matter which provider you use this seems to be the standard. It feels a bit old fashioned in a world of Web Components, native modules, npm packages, transpilers, tree shaking algorithms and many other technologies which can be used to build modern websites these days.

Unfortunately HERE Maps is not different in regard to that. At least two scripts need to be added to import the service submodule.

<script
  charset="utf-8"
  src="http://js.api.here.com/v3/3.0/mapsjs-core.js"
  type="text/javascript"></script>
<script
  charset="utf-8"
  src="http://js.api.here.com/v3/3.0/mapsjs-service.js"
  type="text/javascript"></script>

The website we build for the Beatles album is build with Angular and written in TypeScript. This poses the question of how to use HERE Maps inside the TypeScript code.

The easiest and quickest solution is to just declare H as any. H is the object that HERE Maps attaches to the window. It holds a reference to all available submodules like (clustering, data, geo and others). Defining H as any is essentially telling TypeScript to ignore it.

declare const H: any;

This approach works well but it also defeats the purpose of using TypeScript. TypeScript can't catch any type errors of something it ignores.

It is also possible to tell TypeScript about the type of a global variable by using a hand crafted type definition. This is usually done by using an npm package out of the @types scope. And sure enough there is also @types/heremaps which provides type definitons for HERE Maps.

While this gives you certain type safety it is an all-in-one approach. H is now defined as an ambient type. This basically means it is available everywhere. It will also contain all the types for all the submodules that you could load from HERE Maps. It doesn't reflect the number of submodules loaded at runtime. You could work around that shortcoming by adding a check before accessing anything from the global H object.

if (typeof H === 'object' && 'Map' in H) {
  // Now it should be safe to use H.Map.
}

The problem is that you have to remember to wrap any usage of H in such a check. There is no check at compile time that will warn you if you forget it. TypeScript will just assume that everything is loaded as expected. If something goes wrong you end up with a runtime error thrown in the browser.

Luckily it is now possible to combine the best parts of both approaches. TypeScript as of version 3 has support for a new type called unknown. Unknown sounds similar to any but differs in an important aspect. If you want to use a variable which is of type unknown you have to prove that it is what you expect before you can use it. This is perfect for our use case. We don't know what type H has. It could be loaded already but it could also be still in the process of being evaluated or maybe the download failed and H is not available at all. Defining H as unknown seems to be a good start.

const H: unknown = (<any> window).H;

But how can we prove that H is loaded and holds a reference to the submodules we want to use? This can be done with a guard in TypeScript. A guard is just a function which returns true if the input is of the expected type. The following function would for example assure TypeScript that the given value is of type number.

const isNumber = (value: any): value is number => {
  return typeof value === 'number';
};

I used the same principle to write a set of slightly more advanced type guards for HERE Maps. I ended up publishing them as a package on npm which can be used as shown in the following example.

import { H, isHereMaps } from 'here-maps-type-guards';

// This will not compile as H is still unknown.
console.log(H.map);

if (isHereMaps(H)) {
  // This will work. H is now believed to be available.
  console.log(H.map);
} else {
  // This block will be executed if the check fails.
  console.log('HERE Maps is missing.');
}

The here-maps-type-guards package can not only be used to check for the core of HERE Maps it also provides a guard for each submodule. These guards can also be combined with each other. Please have a look at the readme for all available guards.

It would of course be nice if HERE Maps provided an npm package written in modern JavaScript and accompanied with type definitions which could be transpiled and minified at will. But until that happens using unknown in combination with guards brings us one step closer to the perfect solution.