Build your First Typescript Package in Under 10 Minutes
Tags: typescript, react, tutorial, beginners
Published: 2020-07-28 15:42:30

I've been putting off building my own Typescript packages for a while.

Not for a lack of ideas, but because I know modern Javascript/Typescript development is a mess.  Just check the size of your node_modules directory after starting a default React project to see what I mean, over 200MB of dependencies just to get started!  Or better yet, try to start a React project without `create-react-app`.

It would take me days to configure my own Typescript package with Babel, Prettier, Rollup, ESLint, Jest, etc. just the way I want it. Not to mention probably cost me my sanity.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/hrcithhp57fe2fpc7d0y.jpg)

Then I stumbled onto [TSDX](https://github.com/formium/tsdx).

After reading the [README](https://github.com/formium/tsdx/blob/master/README.md), I was able to publish an npm package complete with tests in **a single evening**.

This guide is a simplified version of what I learned publishing my first package.  By the end of this tutorial you should have a published and tested Typescript package in the [NPM registry](https://www.npmjs.com/).

# NPM Registry

First you'll need to create an NPM account and configure it to use in your command line.  Start with [this short guide](https://docs.npmjs.com/creating-a-new-npm-user-account) to configure your account and login via the command line with `npm login` if you haven't done so.

# What we're Building

Since this tutorial is aimed at beginners, we're going to be building something simple.  A reusable React component with Jest tests, types, and Github actions:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/k36rqjc1wc9ioejidfgl.gif)

Truly awe inspiring, I know.

### [Live Demo](https://codesandbox.io/s/keen-bose-3ymkf)

### [Final Source Code](https://github.com/Buuntu/toggle)

# Setup

Let's bootstrap our TSDX project from the command line:
```bash
npx tsdx create toggle
```

At the prompt, select React:
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/3j19syx9mx59jlpxa80z.png)

After the dependencies are installed, let's make sure we can start the project in development/watch mode:
```bash
cd toggle
npm start
```

You should now have a functioning package!

# File Structure

```bash
> tree -L 2 -I node_modules
.
├── LICENSE
├── README.md
├── dist
│   ├── index.d.ts
│   ├── index.js
│   ├── toggle.cjs.development.js
│   ├── toggle.cjs.development.js.map
│   ├── toggle.cjs.production.min.js
│   ├── toggle.cjs.production.min.js.map
│   ├── toggle.esm.js
│   └── toggle.esm.js.map
├── example
│   ├── index.html
│   ├── index.tsx
│   ├── package.json
│   └── tsconfig.json
├── package-lock.json
├── package.json
├── src
│   └── index.tsx
├── test
│   └── blah.test.tsx
└── tsconfig.json
```

The default project is pretty minimalist.  There are a few directories/files that are important to know though.

### Directories
* src: This is where all of the source files that will be built live
* example: An example playground to test your component/package
* dist: What will get built and published to npm.  You shouldn't really have to touch this directory and it should be excluded from source control.
* test: Your tests

### Files
* src/index.tsx: Your main source file that will be built.  This needs to import all your other source files
* package.json: Dependencies/all configuration for your package
* example/package.json: Dependencies for your playground (these will not be published to npm)
* example/index.tsx: File that loads your package for the playground
* test/blah.test.tsx: Example test file
* README.md: Generated README with a lot of useful information for reference.

# Toggle Component

To keep with React best practices, we'll make a separate file for our component. 

Copy and paste the following code into `src/Toggle.tsx`:

```jsx
// Inside src/Toggle.tsx
import React, { FC } from 'react';

export const Toggle: FC = () => {
  return (
    <label className="switch">
      <input type="checkbox" />
      <span className="slider round"></span>
    </label>
  );
};
```

Nothing crazy here, just a default HTML checkbox.  Let's export our component from our `index.tsx` file which is the main file that will be used in the package.

```jsx
// src/index.tsx

export * from './Toggle';
```

TSDX projects come with an example folder to help you visualize your component in a browser.  This is what we'll use as a sandbox for our component as well.  Since we changed the name of the component, we'll have to update the example import:

```jsx
// example/index.tsx

import 'react-app-polyfill/ie11';
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { Toggle } from '../src/index'; // 👈 Change our import 

const App = () => {
  return (
    <div>
      <Toggle />{/* 👈 Change to use your new component*/}
    </div>
  );
};

ReactDOM.render(<App />, document.getElementById('root'));
```

Now let's run this sandbox environment to see what we have:

```bash
cd example
npm i
npm start
```

Navigate to http://localhost:1234.  You should see a checkbox!

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/rcaqsmvqxwh7nftnyheo.png)

# Styling

Let's add some styles to our sweet checkbox now.  Open a new file called `Toggle.css` inside of the src directory and copy the following styles into it:

```css
/* src/Toggle.css */

/* The switch - the box around the slider */
.switch {
  position: relative;
  display: inline-block;
  width: 60px;
  height: 34px;
}

/* Hide default HTML checkbox */
.switch input {
  opacity: 0;
  width: 0;
  height: 0;
}

/* The slider */
.slider {
  position: absolute;
  cursor: pointer;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: #ccc;
  -webkit-transition: .4s;
  transition: .4s;
}

.slider:before {
  position: absolute;
  content: "";
  height: 26px;
  width: 26px;
  left: 4px;
  bottom: 4px;
  background-color: white;
  -webkit-transition: .4s;
  transition: .4s;
}

input:checked + .slider {
  background-color: #2196F3;
}

input:focus + .slider {
  box-shadow: 0 0 1px #2196F3;
}

input:checked + .slider:before {
  -webkit-transform: translateX(26px);
  -ms-transform: translateX(26px);
  transform: translateX(26px);
}

/* Rounded sliders */
.slider.round {
  border-radius: 34px;
}

.slider.round:before {
  border-radius: 50%;
}
```

Let's import those styles into our `Toggle.tsx` component.  We will need to install the `rollup-plugin-postcss` to tell Rollup how to compile CSS as part of our package:
```bash
npm i -D rollup-plugin-postcss
```

Now create a file called tsdx.config.js in the root of your project and paste the following code:
```jsx
// tsdx.config.js

const postcss = require('rollup-plugin-postcss');

module.exports = {
  rollup(config, options) {
    config.plugins.push(
      postcss({
        plugins: [],
      })
    );
    return config;
  },
};

```

Now we can import our styles using ESM imports:

```jsx
// src/Toggle.tsx

import React, { FC } from 'react';
import './Toggle.css'; // 👈 Import our new styles

export const Toggle: FC = () => {
  return (
    <label className="switch">
      <input type="checkbox" />
      <span className="slider round"></span>
    </label>
  );
};

```

Save and refresh your browser.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/2pgbzjcxxbaf8s67g5j9.png)

Tada!

# Component Props

But what if we want to actually do something with the state of our toggle component?  It's not very useful as is.

Let's add component props in order to give us more flexibility:

```jsx
// src/Toggle.tsx

import React, { FC } from 'react';
require('./Toggle.css');

export type ToggleType = {
  isOn: boolean;
  handleChange: () => void;
};

export const Toggle: FC<ToggleType> = ({ isOn, handleChange }) => {
  return (
    <label className="switch">
      <input checked={isOn} onChange={handleChange} type="checkbox" />
      <span className="slider round"></span>
    </label>
  );
};

```

We can now pass props into the component and manage the state of it.  Our types will automatically be built and included as part of our project, since we are exporting `ToggleType`.

Let's update our playground to contain this state and make sure the toggle still works:

```jsx
// example/index.tsx

import 'react-app-polyfill/ie11';
import React, { useState } from 'react';
import * as ReactDOM from 'react-dom';
import { Toggle } from '../src/index';

const App = () => {
  const [isOn, setIsOn] = useState(false);

  return (
    <div>
      <Toggle isOn={isOn} handleChange={() => setIsOn(!isOn)} />
    </div>
  );
};

ReactDOM.render(<App />, document.getElementById('root'));

```

Now we're handling state *outside* of the component.  This means we can change the toggle state anywhere by simply calling `setIsOn(!isOn)`.

# Tests

We're ready to publish our package, however let's make sure we have a functioning test first.  We want people to contribute to your project and we don't want to test the functionality in our sandbox every time a new PR is opened.

Let's rename the `blah.test.tsx` file to `toggle.test.tsx` and update our `react-dom` render method:

```jsx
// src/tests/blah.test.tsx -> src/tests/toggle.test.tsx

import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { Toggle } from '../src';

describe('it', () => {
  it('renders without crashing', () => {
    const div = document.createElement('div');
    ReactDOM.render(<Toggle isOn={false} handleChange={() => {}} />, div);
    ReactDOM.unmountComponentAtNode(div);
  });
});
```

In order for Jest to be able to read CSS files, we'll need to install a package to allow us to mock these files:

```bash
npm i -D identity-obj-proxy
```

And then edit our package.json to reflect this:

```
// package.json
...
  "jest": {
    "moduleNameMapper": {
      "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/__mocks__/fileMock.js",
      "\\.(css|less|scss|sass)$": "identity-obj-proxy"
    }
  },
...
```

See [Jest docs](https://jestjs.io/docs/en/webpack#mocking-css-modules) for more on why this is necessary.  We should have a functioning test now, from your root level directory:

```bash
npm test
```

Huzzah!

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/d7ax7zq70b3vjmwps0wp.png)

The only problem is that this is just testing that the component mounts and doesn't break the app doing so.  What we really want to test is that the toggle functionality and `isOn` prop works.

We can use [react-testing-library](https://testing-library.com/docs/react-testing-library/intro) to test our component prop functionality:
```bash
npm i -D @testing-library/react @testing-library/jest-dom
```

Let's update our test file to use some of these new testing methods.  We'll be using the `render` and `fireEvent` methods:
```jsx
// test/toggle.test.tsx

import * as React from 'react';
import { render, fireEvent } from '@testing-library/react';
import { Toggle } from '../src';

it('Should render the toggle and be clickable between states', () => {
  // mock onChange function
  const onChange = jest.fn();

  const { getByTestId, rerender } = render(
    <Toggle isOn={false} handleChange={onChange} />
  );
  // checkbox and parent label components
  const checkbox = getByTestId('Toggle');
  const label = getByTestId('Toggle-label');

  // isOn=false should mean it's unchecked
  expect(checkbox).toHaveProperty('checked', false);

  // Clicking from off -> on
  fireEvent.click(label);
  expect(onChange).toHaveBeenCalledTimes(1);

  // isOn=true should mean it's checked
  rerender(<Toggle isOn={true} handleChange={onChange} />);
  expect(checkbox).toHaveProperty('checked', true);

  // Clicking from on -> off
  fireEvent.click(label);
  expect(onChange).toHaveBeenCalledTimes(2);
});
```

If this is a bit confusing or if you're unfamiliar with react-testing-library, that's okay.  All we're really doing here is rendering the component and making sure `isOn` reflects a checked state and that our `handleChange` function is called every time on click.

Double check that it still works:
```bash
npm test
```

# Publish

You'll want to make sure you update the version, author, and name of your package.  The name should be unique and not taken in the NPM registry.  There are three fields you need to change in your package.json:

```
 "author": "Frodo Baggins",
 "name: "frodo-toggle",
 "version": "1.0.0",
```

The last step is to publish now!

```bash
npm publish
```

If you get an error, it's likely that you either need to a) login again via `npm login` or b) change the package name to be unique.  If you want to see if the package name is available, try searching for it in the [npm registry](https://www.npmjs.com/).

Congratulations, you are now a Typescript package author. 😎

Anyone can now install your package from the command line by running:
```bash
npm i your-toggle-lib # replace this with your package name
```

# Next Steps

There are a few things you could do to make this package better from here.  If you're planning on allowing outside contributors you may want to tweak the default [Github action](https://docs.github.com/en/actions/getting-started-with-github-actions/about-github-actions) that comes with TSDX to run your test suite on new PRs.  This will make sure that outside contributors are not merging in broken changes.

Other possible next steps:
* Add props to change the color and add labels to the toggle button.  
* Add a `size` prop with "small", "medium", and "large" options.
* Add different transitions based on a prop.
* Add [styled-components](https://styled-components.com/) instead of css

The world is your oyster!

Configuration is the most painful part of any project, but libraries like TSDX and create-react-app are amazing at lowering the barrier of entry for newcomers and lazy people (like me).  No one likes spending a day fighting with configuration files.  Hopefully this guide gives you a bit more confidence that you can write your own packages.  I look forward to seeing your projects on Github and npm!