Writing Typescript Typings Files for Third Party Modules

Tyespcript can bring sanity to your big JS projects. When the first big refactor comes around, you'll be a typescript convert for life. Unfortunately, during the early stages of a project typing all the things can seem like ponderous overhead designed to ruin your life.

One of the snag points I ran into was bringing in third party modules. The vast majority of common npm modules already have high quality typings files which you can manage through typings, but inevitably you'll run into that necessary little package that doesn't have the typings you need.

Our project needed one such package, googleapis.

In this post I'll go over how to write typings for the generateAuthUrl method which gives a pretty solid overview for how to fill in any other methods you might need.

If you're not the prosaic type and just want to see the example code, check it out on github.

Setup

Make sure you have typescript set up and installed on your machine. Create a tsconfig.json file which will dictate our typescript compiler options. Enter the following settings.

{
    "compilerOptions": {
        "target": "es6",
        "module": "commonjs",
        "sourceMap": true
    }
}

Create a main.ts file. Running tsc in the project directory should compile without errors.

Run npm init and npm install --save googleapis to get the as of yet untyped package.

In the main.ts file add in the following code.

import * as googleApis from 'googleapis';  
const Oauth2 = googleApis.auth.OAuth2;  
import { CLIENT_ID, CLIENT_SECRET, REDIRECT_URI } from './credentials';

const oauth2Client = new Oauth2(CLIENT_ID, CLIENT_SECRET, REDIRECT_URI);

const scopes = [  
  'https://www.googleapis.com/auth/plus.me',
  'https://www.googleapis.com/auth/calendar'
];

const url = oauth2Client.generateAuthUrl({  
  access_type: 'offline', // 'online' (default) or 'offline' (gets refresh_token) 
  scope: scopes // If you only need one scope you can pass it as string 
});

console.log(url);  

If you want to test it out, make sure to add in your own google oauth tokens.

As it is, without some sweet typings, this will not compile to js.

Creating Declaration Files

Create a types directory with the following structure.

types  
├── googleapis
│   └── index.d.ts
└── index.d.ts

Typescript interprets *.d.ts files as type declaration files which will describe the shape of an external library without defining implementation details.

In our types directory we have one top level index.d.ts which will hold a reference to each of our module specific declaration files each of which will contain the actual typings for each module.

In this case, types/index.d.ts will hold one reference to the as of yet unwritten googleapis/index.d.ts file.

/// <reference path="googleapis/index.d.ts" />

Depending on your tsconfig.json settings, this file isn't always necessary. If your types directory is in a sibling or sub directory from tsconfig and you're grabbing all .ts files, typescript will find it for you. If your project structure is different, you'll have to make sure and include it specifically.

Writing Typings

All of the typing heavy lifting will be done in types/googleapis/index.d.ts. The first thing we need to do is create an ambient module. Ambient modules are type declarations that don't define any of the nitty gritty of what the code actually does, but just defines its shape. We will declare a module with the same name as the npm module. As the googleapis export is an object, our ambient module will export a const variable implementing an interface that will contain our typings. This exported typed value can mirror whatever the module exports whether it's a function, object or constructor.

declare module 'googleapis' {

    interface GoogleApi {
    }

    const GoogleApi: GoogleApi;

    export = GoogleApi;
}

If we attempt to compile main.ts at this point, it will allow us to import googleapis, but, as we haven't defined anything on the interface, tsc will throw an error when we try to access any of googleapis properties or methods.

At this point, it's a matter of ascertaining the shape of the module's properties and the signatures of its methods.

Using the module's documentation, we need to determine the arguments and the return values so we can assign appropriate types.

We are interested in the googleapis.auth.Oauth2 constructor function, and the generateAuthUrl method on the object that it returns so we can begin by defining the auth property, which returns an object of type Oauth2Constructor.

...
    interface Oauth2Constructor {
    }

    interface GoogleApi {
        auth: {
            OAuth2: Oauth2Constructor;
        };
    }

    const GoogleApi: GoogleApi;
...

The Oauth2Constructor is a newable constructor function, which we can indicate to typescript by including the new keyword in front of its function definition, that requires three strings as arguments. The constructor in turn returns what we'll call the Oauth2Client which has the generateAuthUrl function we're interested in. Again, using the documentation, we'll define the function signature for the method, extracting out the options argument it requires into its own interface.

...
    interface UrlOptions {
        access_type: string;
        scope: string[];
    }

    interface Oauth2Client {
        generateAuthUrl(opts: UrlOptions): string;
    }

    interface Oauth2Constructor {
        new (GoogleClientId: string, GoogleClientSecret: string, GoogleCallbackUri: string): Oauth2Client;
    }

    interface GoogleApi {
        auth: {
            OAuth2: Oauth2Constructor;
        };
    }
...

That is the essential pattern for writing your own type definition files. You only need to define the pieces of the module that you are currently using, and can always expand it as you consume more of the module's functionality.