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.
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)
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};
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
)
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.
Install Typescript in your project by running:
1$ npm i -D typescript
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}
We are configuring a few important parameters here:
outDir
- where to place generated .js files.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.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.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.include
- where to look for .ts files (src
).exclude
- which folders to avoid (node_modules
) - since we don't want to transpile existing modules and build
our output folder.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}
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 },
Let's try it out:
1$ npm run build
You should see a new folder called build
with three files in it:
index.js
- the compilation output.index.js.map
- the source-map.index.d.ts
- the type definitions. Since we've exported one function, it will be included there.1$ npm i -D ts-jest jest @types/jest
Then, edit package.json
to activate Jest's Typescript support by adding:
1"jest": {
2 "preset": "ts-jest",
3 "testEnvironment": "node"
4 }
and replace
1"test": "echo \"Error: no test specified\" && exit 1"
with
1"test": "jest"
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});
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.
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.
require
function).import
syntax).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
Now, let's edit our package.json
file to use Rollup for the build:
1 "scripts": {
2 "build": "rollup -c",
3 "test": "jest"
4 },
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",
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};
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
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],
Now that we have a working Typescript module, we can proceed with adding React and implementing our component!
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
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 },
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}
This is a simple component that receives a text
prop and renders a happy, red hello message.
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});
This simple test case will:
../index.tsx
.react-test-renderer
.Run npm test
to validate that everything is working as expected.
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:..
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;
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.
Now your component is ready for the real world, just waiting for you to hit npm publish
!
There's still more stuff you can do to optimize your workflow while developing components in React with Typescript:
We'll leave this as an exercise for you learners.