Author avatar

Eli Yukelzon

How to Create a Typescript and React Module

Eli Yukelzon

  • Mar 22, 2019
  • 13 Min read
  • 951 Views
  • Mar 22, 2019
  • 13 Min read
  • 951 Views
Web Development
React

Introduction

The Javascript ecosystem benefited greatly from NPM by proliferating ideas of code-reuse and modular design. Creating a new Javascript module and distributing it is as simple as creating a package.json file, pointing to the Javascript module, and running npm publish. When dealing with Typescript and React, the process is a little bit more involved and that's what we'll discuss in this guide. To make it interesting, we'll add support for testing your component and for setting up a comfortable development workflow.

Plain Javascript Module

Let's start by going through the setup of a plain Javascript NPM module by calling npm init in a clean folder and following the prompts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
$ npm init
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.

See `npm help json` for definitive documentation on these fields
and exactly what they do.

Use `npm install <pkg>` afterwards to install a package and
save it as a dependency in the package.json file.

Press ^C at any time to quit.
package name: (test-module)
version: (1.0.0)
description: My Module!
entry point: (index.js)
test command:
git repository:
keywords:
author:
license: (ISC)
About to write to package.json:

{
  "name": "test-module",
  "version": "1.0.0",
  "description": "My Module!",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}

Is this OK? (yes)
bash

Now, let's create the module file (its name is specified in the main section of package.json):

1
2
3
4
5
module.export = {
  testMethod: function(param) {
    return "Hello " + param;
  }
};
javascript

Here we declared a module that exports (i.e. makes available for outside use) a single function called testMethod.

To test that the NPM packaging works correctly, we run npm pack and inspect the generated test-module-1.0.0.tgz archive.

And we are done! (Well, not really, since we didn't provide the tests or actually perform the npm publish)

Adding Typescript

Typescript is a transpiler for a super-set of the Javascript language. Since we'll be now writing in a language different from Javascript - we need to set up the build step that will convert the code from Typescript to Javascript.

Project Setup

Install Typescript in your project by running:

1
$ npm i -D typescript
bash

Next, we'll setup Typescript by creating configuration file tsconfig.json :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
  "compilerOptions": {
    "outDir": "build",
    "module": "esnext",
    "target": "es5",
    "lib": ["es6", "dom", "es2016", "es2017"],
    "sourceMap": true,
    "allowJs": false,
    "jsx": "react",
    "declaration": true,
    "moduleResolution": "node",
    "forceConsistentCasingInFileNames": true,
    "noImplicitReturns": true,
    "noImplicitThis": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "suppressImplicitAnyIndexErrors": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true
  },
  "include": ["src"],
  "exclude": ["node_modules", "build"]
}
json

We are configuring a few important parameters here:

  1. outDir - where to place generated .js files.
  2. target - what ECMAScript version to target. Since Typescript is a Javascript super-set, it contains features/syntaxes that are not supported by all browser vendors. We specify the compilation target to maintain syntax compatibility.
  3. sourceMap - while transpiling, Typescript can generate a source-map to allow access to the original Typescript code during the debugging on the created JS file.
  4. declaration - Typescript ecosystem is built around .d.ts, the so-called Type Definitions. Adding this flag will generate the .d.ts file for you and include all exported types and functions in it.
  5. include - where to look for .ts files (src).
  6. exclude - which folders to avoid (node_modules) - since we don't want to transpile existing modules and build our output folder.

Code Changes

Let's start by organizing our folder properly by creating a sub-folder called src (the input for the Typescript compiler, as specified in tsconfig.json include section) and move index.js there while renaming it to index.ts

Next, let's fix the syntax errors that occurred due to the strictness options in the tsconfig.json file so that index.ts will look like:

1
2
3
export function testMethod(param: string) {
  return "Hello " + param;
}
typescript

Finally, let's add a build step to execute the Typescript compiler by editing package.json and adding a build script:

1
2
3
4
 "scripts": {
    "build": "tsc",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
json

Let's try it out:

1
$ npm run build
bash

You should see a new folder called build with three files in it:

  1. index.js - the compilation output.
  2. index.js.map - the source-map.
  3. index.d.ts - the type definitions. Since we've exported one function, it will be included there.

Testing

To implement testing, we'll use the popular Jest framework and its Typescript support module ts-jest. To add it to the project, run:

1
$ npm i -D ts-jest jest @types/jest
bash

Then, edit package.json to activate Jest's Typescript support by adding:

1
2
3
4
"jest": {
    "preset": "ts-jest",
    "testEnvironment": "node"
  }
json

and replace

1
"test": "echo \"Error: no test specified\" && exit 1"
json

with

1
"test": "jest"
json

Now, let's create a test file called function.spec.ts in src/__tests__:

1
2
3
4
5
import { testMethod } from "..";

test("create a new hello", () => {
  expect(testMethod("World")).toBe("Hello World");
});
typescript

Here, we are importing the testMethod function and declaring a test that expects a call to testMethod with the parameter "World" to return "Hello World".

Let's see that the tests pass by running:

1
2
3
4
5
6
7
8
9
10
11
$ npm test
> jest

 PASS  src/__tests__/function.spec.ts
  √ create a new hello (2ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.742s, estimated 2s
Ran all test suites.
bash

Prepare for Publishing

As I've mentioned before, Typescript code needs to be compiled down to Javascript before it's published on NPM. To add confusion to this procedure, we should be aware of different module formats that exist in Javascript ecosystem.

  1. CommonJS - module format used by Node (using require function).
  2. ESM - modern module format (using import syntax).
  3. UMD - Universal Module Definition, a pattern for exporting modules that should work everywhere (not as popular these days).

So, in order to support both CommonJS and ESM simultaneously, we'll need to introduce a slightly more complicated build step by replacing the direct call to the Typescript compiler with a build tool called Rollup. This tool (similar to Webpack) will ingest our Typescript code and spit out Javascript code in two separate output formats. Let's add Rollup and it's required plugins to our project:

1
$ npm i -D rollup rollup-plugin-typescript2 rollup-plugin-commonjs  rollup-plugin-peer-deps-external rollup-plugin-node-resolve
bash

Now, let's edit our package.json file to use Rollup for the build:

1
2
3
4
  "scripts": {
    "build": "rollup -c",
    "test": "jest"
  },
json

And point it to the rollup output files:

1
2
3
"main": "build/index.js",
"module": "build/index.es.js",
"jsnext:main": "build/index.es.js",
json

These settings (main, module and jsnext:main) will be used by other bundlers/tools that will use our package to pick up the appropriate format for them.

As the last step, create Rollup configuration file called rollup.config.js to look like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import typescript from "rollup-plugin-typescript2";
import commonjs from "rollup-plugin-commonjs";
import external from "rollup-plugin-peer-deps-external";
import resolve from "rollup-plugin-node-resolve";

import pkg from "./package.json";

export default {
  input: "src/index.ts",
  output: [
    {
      file: pkg.main,
      format: "cjs",
      exports: "named",
      sourcemap: true
    },
    {
      file: pkg.module,
      format: "es",
      exports: "named",
      sourcemap: true
    }
  ],
  plugins: [
    external(),
    resolve(),
    typescript({
      rollupCommonJSResolveHack: true,
      exclude: "**/__tests__/**",
      clean: true
    }),
    commonjs({
      include: ["node_modules/**"],
      namedExports: {
        "node_modules/react/react.js": [
          "Children",
          "Component",
          "PropTypes",
          "createElement"
        ],
        "node_modules/react-dom/index.js": ["render"]
      }
    })
  ]
};
javascript

All done! Let's test it out by running

1
2
3
4
5
6
7
$ npm run build

> [email protected] build 
> rollup -c

src/index.ts → build/index.js, build/index.es.js...
created build/index.js, build/index.es.js in 141ms
bash

Now, if you run npm pack you might notice that our source files were also included in the resulting tarball. To avoid that and to package only the build folder, add this to your package.json:

1
2
3
"files": [
  "build"
],
json

Adding React

Now that we have a working Typescript module, we can proceed with adding React and implementing our component!

Project Setup

We'll start by adding React to our project:

1
$  npm i -D react-scripts-ts react-dom react @types/react-dom react-test-renderer @types/react-test-renderer @types/react
bash

And specifying React as peer-dependency by adding these lines to package.json:

1
2
3
4
  "peerDependencies": {
    "react": "^16.0.0",
    "react-dom": "^16.0.0"
  },
json

Code Changes

Let’s delete our old module index.ts and create a React component index.tsx (please note that we now use tsx extension to support JSX, and update rollup.config.js accordingly):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
 * @class ExampleComponent
 */

import * as React from "react";

export type Props = { text: string };

export default class ExampleComponent extends React.Component<Props> {
  render() {
    const { text } = this.props;

    return <div style={{ color: "red" }}>Hello {text}</div>;
  }
}
typescript

This is a simple component that receives a text prop and renders a happy, red hello message.

Testing

Let's throw away our function.spec.ts and create a new component test called component.spec.tsx that looks like:

1
2
3
4
5
6
7
8
9
10
11
12
13
import * as React from "react";
import Hello from "..";
import renderer from "react-test-renderer";

test("Component should show 'red' text 'Hello World'", () => {
  const component = renderer.create(<Hello text="World" />);
  const testInstance = component.root;

  expect(testInstance.findByType(Hello).props.text).toBe("World");

  let tree = component.toJSON();
  expect(tree).toMatchSnapshot();
});
typescript

This simple test case will:

  • Import our component code from ../index.tsx.
  • Render a test version of it using react-test-renderer.
  • Verify that the prop-passing works correctly by asserting it's value.
  • Confirm that the Snapshot (i.e. the way the component looked the previous time it was tested) matches.

Run npm test to validate that everything is working as expected.

Development Workflow

As a finishing step, let's create an example application that will use our component as the end-users would:

1
2
3
$ npx create-react-app example
$ cd example
$ npm i -S [email protected]:..
bash

The last command means: "Install the test-module package, but don't take it from NPM, take it from the folder above", thus allowing us to work with our module without publishing it first!

Now let's edit the example/src/App.js file and replace it with:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React, { Component } from "react";
import "./App.css";
import Hello from "test-module";

class App extends Component {
  render() {
    return (
      <div className="App">
        <Hello text="World" />
      </div>
    );
  }
}

export default App;
javascript

Run npm start and you’ll see your brand new, red, 'Hello World' message! Now you can work on your component in the root folder, then type npm run build, and test the results in the example to see how it's actually rendered on the webpage.

Closing Words

Now your component is ready for the real world, just waiting for you to hit npm publish!

Potential Improvements

There's still more stuff you can do to optimize your workflow while developing components in React with Typescript:

  1. You can automatically publish your example as GitHub pages (see gh-pages command).
  2. Add automatic server version management using version-bump-prompt.
  3. Setup CI on Travis.

We'll leave this as an exercise for you learners.

6