Michal Zalecki
Michal Zalecki
software development, testing, JavaScript,
TypeScript, Node.js, React, and other stuff

Creating a TypeScript library with a minimal setup

There are a few major reasons due to which you may find yourself creating a library. One, obviously, is that you have a solution which you would like to share with the Open Source community. The other one is that you need to reuse code across different projects or in the same project but on different platforms.

TypeScript's type system and autocompletion support from the text editors make it a great language for writing a library. What intimidates me when it comes to extracting a part of the codebase to a separate repository is the need for a setup. There are many boilerplates in the wild. Some library boilerplates include rollup or webpack but the TypeScript compiler itself is good enough for building even more complex libraries. Using only the tsc and minimal tsconfig you are able to ship a code ready to run in both node and the browser along with types definitions.

package.json

As you probably know package.json not only keeps a list of dependencies but also allows for defining a set of scripts and information about your package such as name, version, main, author, licence, etc. There are also fields specyfic to TypeScript: types, typings (alias of types), and typesVersions introduced in TypeScript 3.1 to enable support for multiple TypeScript versions.

The first thing to do is initializing a repository and setting a remote.

git init
git remote add origin https://github.com/<username>/<reponame>.git

You want to do it before generating a package.json as npm will be able to set repository, bugs and homepage values. You can also do it later by reinitializing package.json. Run init utility to generate a package.json file:

npm init -y

Skip -y to use an interactive mode. Fill in a description, keywords and an author so other developers can find your great library using an npm's search.

TypeScript is not going to be a dependency required to run the library so you should install it as a development dependency. You can install it globally as well but this is something I try to avoid when I can. Using global dependency makes it harder for other developers to build and contribute to your library.

npm install --save-dev typescript

tsconfig.json

Once you have TypeScript compiler (tsc) installed you can access it to generate a tsconfig.json file.

./node_modules/.bin/tsc --init

Such tsconfig.json contains multiple, possible to set configuration options along with their descriptions. I am used to starting my libraries with the following configuration:

{
  "compilerOptions": {
    "target": "ES2015",
    "module": "commonjs",
    "declaration": true,
    "outDir": "lib",
    "strict": true,
    "removeComments": true
  },
  "include": [
    "src/**/*"
  ]
}

I target ES2015+ environments. Current LTS version of Node.js is 8.9.0 which supports 99% of the specs according to node.green and since Edge, Firefox, Chrome, and Safari support 96%-99% of the spec you might not need to use Babel at all. We want also to generate declaration files so we can preserve type safety and once you get back to using the library after some time you do not have to remember the API, your editor does. Configuration for outDir and what files are going to be included is arbitrary.

In case of splitting your library across multiple files, you may end up with multiple compiled files inside the lib directory. If that's not what you aim for you can decide to build entire library into a single file. It's possible to achieve with the outFile option when you target SystemJS or AMD modules.

{
  "compilerOptions": {
    "target": "ES2015",
    "module": "amd",
    "declaration": true,
    "outFile": "lib/index.js",
    "strict": true
  },
  "include": [
    "src/**/*"
  ]
}

build script

We don't need anything sophisticated just to show how to set it up. The file is placed in the src directory which aligns with what I have set in tsconfig.json.

// src/index.ts

export function sum(a: number, b: number) {
  return a + b;
}

To compile this code and place it in the lib it is now enough to call tsc from the directory containing tsconfig.json file.

./node_modules/.bin/tsc

Calling tsc from node_modules is not very convenient. It's easier to follow convention and define a build script. The other thing is that keeping an already built version of the library inside the repository introduces unnecessary noise. I want to ignore entire lib directory with git and use prepare script to build the library once it is published and pushed to npm's registry.

{
  "main": "lib/index.js",
  "typings": "lib/index.d.ts",
  "scripts": {
    "prepare": "npm run build",
    "build": "tsc"
  },
  "files": [
    "lib"
  ]
}

When a library is required the main field in package.json points to a file which becomes a default entry point. You also need to provide a path to types definition. Using files field it is possible to narrow down files which are going to be included when we install our package as a dependency. This is an optional configuration and does not have to be set to publish and use the library.

test script

Setting up a test suit for TypeScript code base is particularly easy with Jest. It is mostly due to preprocessing capabilities which removes the need to compile TypeScript files explicitly before running the tests.

npm install --save-dev jest ts-jest @types/jest

Configuration for Jest goes to the package.json file. We're interested in using both TypeScript and JavaScript modules. Those last ones are used by Jest internally. We want to transform TypeScript modules with ts-jest. Default pattern used by Jest has to be changed too so it matches TypeScript files.

{
  "scripts": {
    "test": "jest"
  },
  "jest": {
    "transform": {
      "^.+\\.tsx?$": "ts-jest"
    },
    "moduleFileExtensions": [
      "ts",
      "js"
    ],
    "testRegex": "^.+\\.spec\\.ts$"
  }
}

We can finally write a few tests for our library.

// src/sum.spec.ts

import { sum } from "./index";

describe("sum", () => {
  it("sums two numbers", () => {
    expect(sum(1, 2)).toEqual(3);
  });
});

That is it!

By no means, it is everything you can do to your library. When you get an attention from the developers' community you would like to set up git hooks. Husky is a great tool to do that and e.g. force run tests before pushing to the repository. Continuous integration will make your project easier to maintain as well. None of those is crucial though. Go small with only what is necessary and have fun building libraries!

The code is available for reference on GitHub MichalZalecki/ts-lib-boilerplate.

Photo by Alfons Morales on Unsplash.