DEV Community

Cover image for Writing a Node.js module in TypeScript
Dominik Kundel
Dominik Kundel

Posted on • Originally published at twilio.com

Writing a Node.js module in TypeScript

One of the best things about Node.js is its massive module ecosystem. With bundlers like webpack we can leverage these even in the browser outside of Node.js. Let’s look at how we can build a module with TypeScript usable by both JavaScript developers and TypeScript developers.

Before we get started make sure that you have Node.js installed – you should ideally have a version of 6.11 or higher. Additionally make sure that you have npm or a similar package manager installed.

Let’s build a module that exposes a function that filters out all emojis in a string and returns the list of emoji shortcodes. Because who doesn’t love emojis?

decorative gif of different emojis

✨ Installing dependencies

First create a new directory for your module and initialize the package.json by running in your command line:

mkdir emoji-search
cd emoji-search
npm init -y
Enter fullscreen mode Exit fullscreen mode

The resulting package.json looks like this:

{
  "name": "emoji-search",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}
Enter fullscreen mode Exit fullscreen mode

Now let’s install some dependencies. First install the TypeScript compiler as a devDependency by running:

npm install typescript --save-dev
Enter fullscreen mode Exit fullscreen mode

Next install the emojione module. We’ll use this to convert emojis to their shortcodes like 🐵 to :monkey_face:. Because we will be using the module in TypeScript and the module doesn’t expose the types directly we also need to install the types for emojione:

npm install emojione @types/emojione --save
Enter fullscreen mode Exit fullscreen mode

With the project dependencies installed we can move on to configuring our TypeScript project.

🔧 Configuring the TypeScript project

Start by creating a tsconfig.json file which we’ll use to define our TypeScript compiler options. You can create this file manually and place the following lines into it:

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "declaration": true,
    "outDir": "./dist",
    "strict": true
  }
}
Enter fullscreen mode Exit fullscreen mode

Alternatively you can auto-generate the tsconfig.json file with all available options by running:

./node_modules/.bin/tsc --init
Enter fullscreen mode Exit fullscreen mode

If you decided for this approach just make sure to adjust the declaration and outDir options according to the JSON above.

Setting the declaration attribute to true ensures that the compiler generates the respective TypeScript definitions files aside of compiling the TypeScript files to JavaScript files. The outDir parameter defines the output directory as the dist folder.

Next modify the package.json to have a build script that builds our code:

{
  "name": "emoji-search",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "tsc",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "typescript": "^2.3.2"
  },
  "dependencies": {
    "@types/emojione": "^2.2.1",
    "emojione": "^3.0.3"
  }
}
Enter fullscreen mode Exit fullscreen mode

That’s all we have to do to configure the TypeScript project. Let’s move on to writing some module code!

💻 Create the module code

Create a lib folder where we can place all of our TypeScript files and in it create a create a file called index.ts. Place the following TypeScript into it:

import { toShort } from 'emojione';
const EMOJI_SHORTCODES = /:[a-zA-Z1-9_]+:/g

export function findEmojis(str: string): string[] {
  // add runtime check for use in JavaScript
  if (typeof str !== 'string') {
    return [];
  }

  return toShort(str).match(EMOJI_SHORTCODES) || [];
}
Enter fullscreen mode Exit fullscreen mode

Compile the code by running:

npm run build
Enter fullscreen mode Exit fullscreen mode

You should see a new dist directory that has two files, index.js and index.d.ts. The index.js contains all the logic that we coded compiled to JavaScript and index.d.ts is the file that describes the types of our module for use in TypeScript.

Congratulations on creating your first module accessible to both TypeScript and Javascript! Lets prep the module for publishing.

🔖 Prepare for publishing

Now that we have our module, we have to make three easy changes to the package.json to get ready to publish the module.

  1. Change the main attribute to point at our generated JavaScript file
  2. Add the new types parameter and point it to the generated TypeScript types file
  3. Add a prepublish script to make sure that the code will be compiled before we publish the project.
{
  "name": "emoji-search",
  "version": "1.0.0",
  "description": "",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "scripts": {
    "prepublish": "npm run build",
    "build": "tsc",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "typescript": "^2.3.2"
  },
  "dependencies": {
    "@types/emojione": "^2.2.1",
    "emojione": "^3.0.3"
  }
}
Enter fullscreen mode Exit fullscreen mode

We should also make sure to exclude unnecessary files from the installation of our module. In our case the lib/ folder is unnecessary because we only need the built files in the dist/ directory. Create a new file called .npmignore and place the following content into it:

lib/
Enter fullscreen mode Exit fullscreen mode

That’s it! 🎉 You are ready now to publish your module using npm publish. Unfortunately someone already built a module called emoji-search 😕 so if you want to publish this module, just change the name in the package.json to another name.

🍽 Consume the module

The great thing with our module is that this can now be seamlessly used in JavaScript or TypeScript projects. Simply install it via npm or yarn:

npm install emoji-search --save
Enter fullscreen mode Exit fullscreen mode

decorative gif of flying emojis

If you want to try this out without publishing the module yourself you can also install the demo-emoji-search module. It is the same code published on npm. Afterwards we can use the module in JavaScript:

const emojiSearch = require('demo-emoji-search');
console.log(emojiSearch.findEmojis("Hello 🐼! What's up? ✌️"));
Enter fullscreen mode Exit fullscreen mode

Or in TypeScript with full type support:

import { findEmojis } from 'demo-emoji-search';
const foundEmojis: string[] = findEmojis(`Hello 🐵! What's up? ✌️`);

console.log(foundEmojis);
Enter fullscreen mode Exit fullscreen mode

🎊 Conclusion

Now this was obviously just a very simple module to show you how easy it is to publish a module usable in both Javascript and TypeScript.

There are a boatload of other benefits provided by TypeScript to the author of the module such as:

  • Better authoring experience through better autocomplete
  • Type safety to catch bugs especially in edge cases early
  • Down-transpilation of cutting-edge and experimental features such as decorators

As you’ve seen it’s very easy to build a module in TypeScript to provide a kickass experience with our module to both JavaScript and TypeScript developers. If you would like to have a more comprehensive starter template to work off that includes a set of best practices and tools, check out Martin Hochel’s typescript-lib-starter on GitHub.

✌️ I would love to hear about your experience with TypeScript and feel free to reach out if you have any problems:


Writing a Node.js module in TypeScript was originally published on the Twilio Blog on June 8, 2017.

Discussion (8)

Collapse
rauschma profile image
Axel Rauschmayer

Watch out – prepublish is deprecated: docs.npmjs.com/misc/scripts#deprec...

Should @types/emojione be dev dependency?

Collapse
dkundel profile image
Dominik Kundel Author

prepublish itself is not deprecated. The old behavior is deprecated. But it's not the behavior that I'm intending here.

No the @types/emojione should be a normal dependency because it's a dependency of the generated .d.ts file. Basically the @types of any regular dependency should also be a regular dependency.

Collapse
rauschma profile image
Axel Rauschmayer

prepublish: right. npm’s deprecation warning gave me the wrong impression. I’m using prepublishOnly to avoid that warning, but it looks like that will be deprecated in the long run, while prepublish will stay (with prepublishOnly behavior): github.com/npm/npm/issues/10074

@types/emojione: make sense, didn’t know about dependencies between type definition files.

Thread Thread
dkundel profile image
Dominik Kundel Author

In this case it doesn't make a difference whether @types/emojione is a dev dependency or not because the .d.ts will only expose one signature that has only built-in types. But if we would have a function that for example exposes a Buffer as part of one signature and we would need @types/node as a normal dependency since the generated .d.ts file has a connection to it. So the best practice would be to just do it for all dependencies that you actually use during runtime.

Collapse
phipsy7 profile image
Philippe Rüesch

There's an error in your regex string. It should be
var EMOJI_SHORTCODES = /:[a-zA-Z1-9_]+:/g;
You've forgotten the + at the end of the []

Collapse
dkundel profile image
Dominik Kundel Author

Ah it was dropped during the import from the other blog. Thank you!

Collapse
metric152 profile image
Ernest

Your last code example has one to many ( in it
findEmojis((Hello 🐵! What's up? ✌️);

Collapse
dkundel profile image
Dominik Kundel Author

Good catch! thanks!