Author avatar

Eli Yukelzon

How to Create a Typescript and React Module

Eli Yukelzon

  • Mar 22, 2019
  • 13 Min read
  • 70,574 Views
  • Mar 22, 2019
  • 13 Min read
  • 70,574 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$ npm init
2This utility will walk you through creating a package.json file.
3It only covers the most common items, and tries to guess sensible defaults.
4
5See `npm help json` for definitive documentation on these fields
6and exactly what they do.
7
8Use `npm install <pkg>` afterwards to install a package and
9save it as a dependency in the package.json file.
10
11Press ^C at any time to quit.
12package name: (test-module)
13version: (1.0.0)
14description: My Module!
15entry point: (index.js)
16test command:
17git repository:
18keywords:
19author:
20license: (ISC)
21About to write to package.json:
22
23{
24  "name": "test-module",
25  "version": "1.0.0",
26  "description": "My Module!",
27  "main": "index.js",
28  "scripts": {
29    "test": "echo \"Error: no test specified\" && exit 1"
30  },
31  "author": "",
32  "license": "ISC"
33}
34
35Is this OK? (yes)
bash

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

1module.export = {
2  testMethod: function(param) {
3    return "Hello " + param;
4  }
5};
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  "compilerOptions": {
3    "outDir": "build",
4    "module": "esnext",
5    "target": "es5",
6    "lib": ["es6", "dom", "es2016", "es2017"],
7    "sourceMap": true,
8    "allowJs": false,
9    "jsx": "react",
10    "declaration": true,
11    "moduleResolution": "node",
12    "forceConsistentCasingInFileNames": true,
13    "noImplicitReturns": true,
14    "noImplicitThis": true,
15    "noImplicitAny": true,
16    "strictNullChecks": true,
17    "suppressImplicitAnyIndexErrors": true,
18    "noUnusedLocals": true,
19    "noUnusedParameters": true
20  },
21  "include": ["src"],
22  "exclude": ["node_modules", "build"]
23}
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:

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

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

1 "scripts": {
2    "build": "tsc",
3    "test": "echo \"Error: no test specified\" && exit 1"
4  },
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"jest": {
2    "preset": "ts-jest",
3    "testEnvironment": "node"
4  }
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__:

1import { testMethod } from "..";
2
3test("create a new hello", () => {
4  expect(testMethod("World")).toBe("Hello World");
5});
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$ npm test
2> jest
3
4 PASS  src/__tests__/function.spec.ts
5  √ create a new hello (2ms)
6
7Test Suites: 1 passed, 1 total
8Tests:       1 passed, 1 total
9Snapshots:   0 total
10Time:        0.742s, estimated 2s
11Ran 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  "scripts": {
2    "build": "rollup -c",
3    "test": "jest"
4  },
json

And point it to the rollup output files:

1"main": "build/index.js",
2"module": "build/index.es.js",
3"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:

1import typescript from "rollup-plugin-typescript2";
2import commonjs from "rollup-plugin-commonjs";
3import external from "rollup-plugin-peer-deps-external";
4import resolve from "rollup-plugin-node-resolve";
5
6import pkg from "./package.json";
7
8export default {
9  input: "src/index.ts",
10  output: [
11    {
12      file: pkg.main,
13      format: "cjs",
14      exports: "named",
15      sourcemap: true
16    },
17    {
18      file: pkg.module,
19      format: "es",
20      exports: "named",
21      sourcemap: true
22    }
23  ],
24  plugins: [
25    external(),
26    resolve(),
27    typescript({
28      rollupCommonJSResolveHack: true,
29      exclude: "**/__tests__/**",
30      clean: true
31    }),
32    commonjs({
33      include: ["node_modules/**"],
34      namedExports: {
35        "node_modules/react/react.js": [
36          "Children",
37          "Component",
38          "PropTypes",
39          "createElement"
40        ],
41        "node_modules/react-dom/index.js": ["render"]
42      }
43    })
44  ]
45};
javascript

All done! Let's test it out by running

1$ npm run build
2
3> [email protected] build 
4> rollup -c
5
6src/index.ts → build/index.js, build/index.es.js...
7created 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"files": [
2  "build"
3],
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  "peerDependencies": {
2    "react": "^16.0.0",
3    "react-dom": "^16.0.0"
4  },
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 * @class ExampleComponent
3 */
4
5import * as React from "react";
6
7export type Props = { text: string };
8
9export default class ExampleComponent extends React.Component<Props> {
10  render() {
11    const { text } = this.props;
12
13    return <div style={{ color: "red" }}>Hello {text}</div>;
14  }
15}
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:

1import * as React from "react";
2import Hello from "..";
3import renderer from "react-test-renderer";
4
5test("Component should show 'red' text 'Hello World'", () => {
6  const component = renderer.create(<Hello text="World" />);
7  const testInstance = component.root;
8
9  expect(testInstance.findByType(Hello).props.text).toBe("World");
10
11  let tree = component.toJSON();
12  expect(tree).toMatchSnapshot();
13});
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$ npx create-react-app example
2$ cd example
3$ npm i -S test-module@file:..
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:

1import React, { Component } from "react";
2import "./App.css";
3import Hello from "test-module";
4
5class App extends Component {
6  render() {
7    return (
8      <div className="App">
9        <Hello text="World" />
10      </div>
11    );
12  }
13}
14
15export 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.