DEV Community

Cover image for Build your First Typescript Package in Under 10 Minutes
Gabriel Abud
Gabriel Abud

Posted on • Updated on

Build your First Typescript Package in Under 10 Minutes

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

Then I stumbled onto TSDX.

After reading the README, 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.

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 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

Truly awe inspiring, I know.

Live Demo

Final Source Code

Setup

Let's bootstrap our TSDX project from the command line:

npx tsdx create toggle
Enter fullscreen mode Exit fullscreen mode

At the prompt, select React:
Alt Text

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

cd toggle
npm start
Enter fullscreen mode Exit fullscreen mode

You should now have a functioning package!

File Structure

> 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
Enter fullscreen mode Exit fullscreen mode

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:

// 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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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.

// src/index.tsx

export * from './Toggle';
Enter fullscreen mode Exit fullscreen mode

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:

// 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'));
Enter fullscreen mode Exit fullscreen mode

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

cd example
npm i
npm start
Enter fullscreen mode Exit fullscreen mode

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

Alt Text

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:

/* 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%;
}
Enter fullscreen mode Exit fullscreen mode

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:

npm i -D rollup-plugin-postcss
Enter fullscreen mode Exit fullscreen mode

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

// tsdx.config.js

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

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

Enter fullscreen mode Exit fullscreen mode

Now we can import our styles using ESM imports:

// 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>
  );
};

Enter fullscreen mode Exit fullscreen mode

Save and refresh your browser.

Alt Text

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:

// 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>
  );
};

Enter fullscreen mode Exit fullscreen mode

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:

// 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'));

Enter fullscreen mode Exit fullscreen mode

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:

// 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);
  });
});
Enter fullscreen mode Exit fullscreen mode

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:

npm i -D identity-obj-proxy
Enter fullscreen mode Exit fullscreen mode

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"
    }
  },
...
Enter fullscreen mode Exit fullscreen mode

See Jest docs for more on why this is necessary. We should have a functioning test now, from your root level directory:

npm test
Enter fullscreen mode Exit fullscreen mode

Huzzah!

Alt Text

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 to test our component prop functionality:

npm i -D @testing-library/react @testing-library/jest-dom
Enter fullscreen mode Exit fullscreen mode

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

// 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);
});
Enter fullscreen mode Exit fullscreen mode

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:

npm test
Enter fullscreen mode Exit fullscreen mode

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",
Enter fullscreen mode Exit fullscreen mode

The last step is to publish now!

npm publish
Enter fullscreen mode Exit fullscreen mode

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.

Congratulations, you are now a Typescript package author. 😎

Anyone can now install your package from the command line by running:

npm i your-toggle-lib # replace this with your package name
Enter fullscreen mode Exit fullscreen mode

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 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 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!

Discussion (10)

Collapse
mattpocockuk profile image
Matt Pocock

Would importing the css files as a require('./index.css') mean you'd need a custom webpack config on the other end to pull it through? Not sure if that require would resolve when you used the package. Unsure though, not tried it out myself.

Collapse
g_abud profile image
Gabriel Abud Author • Edited on

I think not using require actually would require a custom rollup plugin from the look of it. When using ESM type imports for CSS I get an error, but maybe I'm missing something?

Something like: github.com/thgh/rollup-plugin-css-.... I'm kind of a noob with Rollup though, more familiar with Webpack and CRA. If there's a better way to do this without having to install a plugin I'm all ears.

Collapse
mattpocockuk profile image
Matt Pocock

Styled-components would support this natively, I think. That should be fairly easy to drop in to your code examples above. Or a similar css-in-js solution.

Either that, or you'd need to allow users to import the CSS file directly, similar to how react-toastify handles it.

What's the error you're getting? A TS error or a Rollup error? (I am also pretty clueless with rollup, also a CRA boi)

Thread Thread
g_abud profile image
Gabriel Abud Author

When running npm start:

✖ Failed to compile
Error: Unexpected token (Note that you need plugins to import files that are not JavaScript)
Collapse
g_abud profile image
Gabriel Abud Author

You were right about that, it didn't work correctly when you pulled it from npm. I added the postcss plugin to the tutorial and now it should work as expected. Thanks for the heads up!

Collapse
mattpocockuk profile image
Matt Pocock

Nice, good stuff.

Collapse
busyzz profile image
busyzz
cd example
npm i
npm start
Enter fullscreen mode Exit fullscreen mode

When I use npm start, I get an error, error: Invalid Version: undefined, what should I do

Collapse
mcjores profile image
MCjores

I faced the same problem. Look at this...
github.com/parcel-bundler/parcel/i...

Collapse
sagar profile image
Sagar • Edited on

tsdx is really awesome tool for creating npm packages similar to microbundler.

Collapse
mahdipakravan profile image
Mahdi Pakravan

great !
it's fantastic
special thanks for your help
have a good dev;