Wednesday, October 11, 2017

Declaration Files in TypeScript: An Introduction

This is the first post in what I believe would be a series where I capture some of the things I learned about TypeScript while developing ip-num, a library for working with IP numbers.

Picking up TypeScript, the language, was relatively painless. As it is often said, it is just JavaScript with types right? so how hard can it get?

What was not relatively painless was the "other things" around the language. Things like tooling, the workflow, the configurations etc. Things you need to know before you can actually use TypeScript to make anything useful.

Declarations files were one of such "other things" that introduced some learning curve due to the lack of a clear and comprehensive explanation on it. There is also a lot of dated information due to historical reasons out there, and this adds to the confusion.

tsd you said? what about typings? Or what is the deal with that @typing thing in the node_module directory? How does a GitHub repository called DefinitelyTyped tie into the picture? What role is npm even playing? etc. These were some of the not so obvious things that needed clarification.

This post captures some things I have come to understand regarding declaration files that make them less confusing. Hopefully, someone else who is just starting with TypeScript will find it useful.

If you have some vague idea of how declaration files work and you just need a quick, straight to the point listing of the key concepts and how they tie together? Then head to the Summary section.

For a more expository explanation, do read on.

This post will be broadly grouped into the following headings:
  1. What exactly are Declaration Files and Typings
  2. Where to find Declaration Files
  3. How to use Declaration Files.

What exactly are Declaration Files

Types, in general, can be described as a way to enumerate and delineate values.

You can think of it as you do sets from basic maths; which is a way to group things, where the grouping makes sense under the context they are being used. So you can have a set of fruits (which will contain things like Oranges, Bananas etc) or sets of shoes (which will contain things like Sneakers, Loafers etc).

In TypeScript, types are also a mechanism that allows us to enumerate/delineate values, but I usually like to think of types, especially in the context of TypeScript, as a mechanism that allows us to describe the shape of things. Once the shape of things is described, we can then say whether a value is of type A or type B based on its described shape.

For example, I can come up with a Person type and describe it to be a thing that has a name and age property (which themselves are of type string), and a method getName to return the name.

I can use interface as a mechanism to describe the Person shape and use Classes that implement the Person interface as a mechanism to say that values created by that class should have the shape as described by the Person interface. I can then create a value that would be of this shape by the mechanism of instantiation.

One out of the various benefits of having the aforementioned mechanism available at our disposal is the ability for IDE’s to reason about the code, and thus offer things like autocompletion as shown in the screenshot:



So while this ability to use Types, to describe and delineate shapes of values comes with TypeScript (well, isn’t that why it is called TypeScript?), you do not have this ability in JavaScript.

Also, TypeScript is also a superset of JavaScript, meaning you can write and use JavaScript libraries from within TypeScript. In such situations how should TypeScript handle the lack of Type information in JavaScript?

There are two options: either accept the lack of Types in JavaScript as a fact of life and move on...

or

...provide an ad-hoc way to specify the shape of things in JavaScript, despite the fact that natively, JavaScript does not have types.

The second option is exactly what declaration files provide. It is a way to describe the shape of JavaScript values to the TypeScript compiler. Or put another way, it is the way to describe, (usually in an external file), the types present in an external JavaScript code.

Type declarations are usually contained in files with a .d.ts extension.

If you ever programmed in C++, you can think of them as TypeScripts’ header files.

Let’s see a simple example to illustrate the functionality of declaration files.

So, below we will have a piece of code that simulates the usage of an object defined with vanilla JavaScript. We would attempt to make use of a method not defined on the object.

Since JavaScript comes with no type information, the Typescript compiler would not be able to warn us of the error at compile/transpile time. We only see things going wrong at runtime. We will then fix things by introducing a type declaration file.

// In file Main.ts
// simulating vanilla Js with no type information
let ajalaJs;
ajalaJs = {
 name: "Ajala the traveller",
 age: 12,
 getName: function() {
     return this.name;
   }
};

let mrAjala = ajalaJs.lol();
console.log(mrAjala);

We then compile this file from the command line:

tsc Main.ts

The compilation is successful and a Main.js file is created, which we run via node:

node Main.js

And at this point we run into a runtime exception:
Main.js:34
var mrAjala = ajalaJs.lol();
                      ^

TypeError: ajalaJs.lol is not a function
    at Object. (/Users/daderemi/Main.js:34:23)
    at Module._compile (module.js:573:30)
    at Object.Module._extensions..js (module.js:584:10)
    at Module.load (module.js:507:32)
    at tryModuleLoad (module.js:470:12)
    at Function.Module._load (module.js:462:3)
    at Function.Module.runMain (module.js:609:10)
    at startup (bootstrap_node.js:158:16)
    at bootstrap_node.js:578:3


The runtime error is telling us that we are trying to call a method "lol" that does not exist on the object. There was no way the TypeScript compiler could have warned us about this during compilation since no type information was available for it.

Let us fix this.

We introduce a type declaration file which will describe the shape of the object stored in the mrAjala variable.

Create a file Main.d.ts and have the content be:

declare module "MyTypes" {
 export interface Person {
  name: string;
  age: number;
  getName():string 
 } 
}


Do not worry about the content of the Main.d.ts file just yet. This is not needed for the current topic at hand. I Probably will be writing a follow-up post touching on how to write declaration files.

Next up, update Main.ts. Include /// <reference path="Main.d.ts"> at the top, and also add the type annotation. ie:

/// <reference path="Main.d.ts" />

import * as Person from "MyTypes"

let ajalaJs:MyTypes.Person

ajalaJs = {
 name: "Ajala the traveller",
 age: 12,
 getName: function() {
  return this.name;
   }
};

let mrAjala = ajalaJs.lol();
console.log(mrAjala);

We then compile the file again by running:

tcs Main.ts

This time around we would be greeted by a compiler error:
Main.ts(15,23): error TS2339: Property 'lol' does not exist on type 'Person'.

With the declaration file provided and included via the /// <reference path="Main.d.ts"> line, the TypeScript compiler now has information about the shape of the Person type and thus can warn us at compile time that we are trying to perform an operation on the mrAjala that is not available.

And basically, that is what type declaration file allows. It is a mechanism for supplying type information about JavaScript to TypeScript.

But then, you might ask, there are countless JavaScript libraries already out there...how then can I get their type information? This question is answered next.

Where to find Declaration Files

So now that we know what declaration files are, the next question is: where to find them?

Declaration files are normal text files with just a .d.ts extension. This means they can be made available via just about any media which allows files to be shared: a private HTTP/FTP server, bower, npm, GitHub etc

But this creates a discovery problem. How does one know which source is hosting a particular type declaration of interest?

This problem has been addressed via two ways. One is historical and the way things were done before the TypeScript 2.0 release...the other approach is after TypeScript 2.0 was released and it is the recommended way to go about things. We go over both. Touching the historical approach will help make sense of some of the outdated information that might be encountered on the web.

Pre TypeScript 2.0 Solution to type declaration discovery

As mentioned earlier, Type declaration file can be hosted on Github. And in the early days, this is exactly what happened. The community coalesced around a Github repository where they contributed declaration files for popular JavaScript library.

The GitHub repository is what came to be known as DefintelyTyped

If all of the declaration files in existence, and will ever exist can be found in this GitHub repository and only in this GitHub repository, then things would be a lot simpler, but as previously mentioned, declaration files can also be found in other sources: bower, npm etc.

This led some member of the community to create tooling around the discovery of declaration files.

The idea was to create a tool that searches not only the DefintelyTyped GitHub repository but also, all the other known public sources where declaration files can be hosted.

tsd was the first attempt at such a tool. But tsd was soon deprecated in favor of the "Typings" project.

The Typings project, amongst other things, consist of the Typings cli, (also referred to as TypeScript definition manager, which is used to search, install, manage declaration files), and the Typing Registry which keeps track of declaration files found in other public sources.
It is worth mentioning again that using either tsd or Typings are no longer the recommended way to work with declaration files. As can be seen from their respective GitHub pages, these tools have since been deprecated. Keep reading for the preferred way.
The Typings cli would search for declaration files from the DefinitelyTyped GitHub repository, and from other public sources being tracked in the Typings Registry. You would have to install the Typings cli to make use of this facility, but as mentioned above, Typings toolchain is deprecated and not the recommended way for dealing with declaration files.

The recommended approach is discussed next

Post TypeScript 2.0 Solution to type declaration discovery

Without a doubt, npm has grown to be the defacto registry for JavaScript libraries. Won't it then not be nice to have npm as the main source of TypeScript declaration files?

This is exactly the approach that the TypeScript team decided to take with TypeScript 2.0. Where getting a type declaration is done exactly the same way you get any other npm artifact.

For example, getting the type declaration for Jquery into your project will be:

npm install --save @types/jquery

Getting the type declaration for express will be:

npm install --save @types/express

etc.

What then happens to all the declarations in the DefinitelyTyped GitHub repository? Can they also be accessed via npm?

The answer is yes.

The TypeScript team has built a tool that periodically publishes all of the type declaration contained within the DefinitelyTyped GitHub repository to npm, putting the type declarations under the @types namespace in npm.

This explains why you have the @types in the npm install examples above.

Types-publisher, is the tool that takes care of taking the content of DefinitelyTyped and publishing to npm.

The TypeScript team has also developed a tool for searching type declarations. It is called TypeSearch and can be seen here.

So the answer to the question, how do I find the type declaration for a JavaScript library? Use TypeSearch. The type declaration you looking for is most probably already in the DefinitelyTyped GitHub repository, TypeSearch makes it easier to search the repository.

How to use Declaration Files.

So now, we know what declaration files are, and where to find them. The next question then is, how exactly do we make use of these declaration files in our code base? How do we bring them in and incorporate them?

Again, you have two approaches. The first approach is how things were pre TypeScript 2.0 and would only be touched upon here for completeness. The second approach presented will be the recommended approach post TypeScript 2.0

Incorporating type declaration pre TypeScript 2.0

Before TypeScript 2.0, tsd, or the typing cli would be used to download the type declaration into a location within your project. The type declaration will then be included into the required .ts file via what is called triple slash directive.

So for example, assuming the typings cli has downloaded a type declaration in the location /project/path/to/types, then at the very top of a typescript file that needs the declaration, the following piece of code needs to be added:

/// <reference path="/project/path/to/types">

The triple slash directives are a way of passing pre-processing instructions to the TypeScript compiler. They come in various forms. The /// <reference path="..."> form is used to declare dependencies between file, making sure that the file location pointed to by the path property get included in the file before compilation.

You can see the TypeScript documentation for more information about triple slash declarations

Incorporating type declaration post TypeScript 2.0

With TypeScript 2.0, as mentioned, npm is used to manage type declaration.

When npm is used to install a type declaration, eg:

npm install --save @types/lodash

The lodash type declaration file would be downloaded and saved within:

/project_root/node_modules/@types directory.

What then needs to be done so as to include these type declaration in .ts file that needs them?

Nothing!

With TypeScript 2.0, when a type declaration is included within the /project_root/node_modules/@types directory, nothing extra needs to be done to have the type declaration accessible from .ts files within the project. The typescript compiler would automatically find these installed type declarations and make them available during compilation.

There are two properties of the tsconfig.json that can be used to modify this behavior. tsconfig.json being the file that is used to configure the behavior of the TypeScript compiler within a project.

Even though for most of the time, the default behavior would be what is needed, it is still useful to be aware of the configuration options via the tsconfig.json file.

The two properties in the tsconfig.json files are "typeRoots" and "types" eg:

{
"typeRoots": [],
"types": []
}

typeRoots is used to configure the compiler with array of locations where the TypeScript compiler should look into to find declaration files. The default is ./node_modules/@types (or more specific the @types directory in all the locations node searches for while resolving modules, so ./node_modules/@types/, ../node_modules/@types/, ../../node_modules/@types/ etc)

In case you want to change this default location, or perhaps include other custom locations, then typeRoots is the configuration that provides this opportunity. For example:

"typeRoots": [
"./node_modules/@types",
"./custom_locations"
]

Means the TypeScript compiler will look into both ./node_modules/@types and ./custom_locations when searching for declaration files.

The other property, the types property, is used to list out the actual type declarations files to include from all the ones found in the location(s) specified by the typeRoots.

When not configured, the TypeScript compiler would make available all declaration files within the typeRoots.

For completeness sake, I would also mention that it is still also possible to use triple slash directives to include type declarations. It is not a common approach, neither a recommended approach.

In case you still need to use them, the form of such triple slash directive would be:

/// <reference types="lodash">

Notice this form of the triple slash directive has types as its properties (and not path like the one we already saw)

With this form, the TypeScript compiler would automatically look into the /node_modules/@types directory for the type declaration. Thus for /// <reference types="lodash"> the compiler would look at /project_root/node_modules/@types/lodash/index.d.ts

Summary aka tl:dr

  • Type declarations are ways of providing Type information about JavaScript code base (which by their nature of being JavaScript lacks any type information) to the TypeScript compiler.
  • The type declarations are usually in external files with a .d.ts extension.
  • DefinitelyTyped is a community project that hosts declaration files of popular JavaScript library on GitHub.
  • In some instance, type declarations are referred to as typings
  • Typings could also mean the community-driven tool that was developed to manage: find, install declaration files.
  • Before the Typings tool was developed, there was the tsd tool. Which does the same thing: help manage: find, install etc declaration files.
  • tsd and Typings tool are both deprecated and not the recommended way for managing declaration files
  • With TypeScript 2.0 npm is the recommended tool for managing declaration files
  • If you encounter old code bases or happen to have to use tsd/typing, you will need to use the triple slash directive to tell your TypeScript code base where the needed declaration files are. The triple slash directive takes the form /// <reference path="path_to_declaration_file">
  • When managing declaration files with npm, the TypeScript compiler would automatically find the declaration files, thus no need for using the triple slash directive.
  • Even though triple slash directive is not needed, there is also a form that could be used. It takes the form of /// <reference types="name_of_library">
  • "typeRoots" and "types" are the two properties of the tsconfig.json file that can be used to configure the behavior of the type declaration resolution. 
  • You can use the TypeSearch tool to search the DefinitelyTyped repository for type declarations

No comments:

Post a Comment