How to Develop TypeScript NPM Packages for React Native

Exploring TypeScript based NPM development and publishing in the context of React Native

Introduction

This piece explores the development and management of TypeScript-based NPM packages for React Native. It will cover how to configure TypeScript linting and tsconfig for a package to ensure code integrity with useful VS Code extensions and settings to aid in TypeScript development.

An entire section will be dedicated to TypeScript specific development of a package, in addition to visiting a demo packages project that is available on GitHub. But there are other important aspects of developing NPM packages worth considering. Let’s firstly highlight some of those aspects so the reader gets a good intuition of what will be covered throughout this piece.

Package development: standalone vs embedded

Developing packages can either be done in a standalone project or within a project where those packages are installed and used. The latter approach makes sense if you build components for a particular project (a React Native app for example) and abstract those components into packages further down the line as they are needed for other projects.

This ties into the concept of Agile Development where there may be no concrete roadmap to what components will be abstracted into packages.

The workflow of agile package development.

This approach has the advantage of having package dependencies already in place, such as the peer dependencies that packages may require.

A peer dependency is a dependency a package requires but is not installed within its node modules. Instead, it relies on the installed version of the host project.

We’ll cover the types of dependencies and workspace setup in more detail further down.

Scopes can be used to speed up package development

We’ll also cover how to use scoped packages as a way of grouping your related packages under one name — e.g. @my-org/<package_name>, allowing you to house all your packages under one organisation or branch. NPM or Yarn commands can then be executed on all packages under that scope simultaneously, thus speeding up development.

Upgrading all packages under a scope with yarn upgrade.

Private NPM Registry Solutions

Private registries allow developers to host their own packages in a private manner to compliment their internal ecosystem of app building blocks, and such a workflow has been gaining momentum in adoption. Private registries allow a developer or team to manage packages on their machine, or on a remote server in a team setting that requires authentication to access.

The NPM registry of choice plays a big part of your development pipeline. This article will cite the Verdaccio private proxy registry as the solution to publishing packages. As Verdaccio acts as a proxy to the public NPM registry, it is flexible in that you can store your private packages on Verdaccio while simultaneously having access to the public NPM registry.

I have published a separate piece on Verdaccio that deep dives into setting up a remote private registry for your development team. Check out that piece here: Publish Private NPM Packages with Proxy Registry Verdaccio.

Private registries play a huge role in large entities where libraries of packages need to be stored and maintained under that particular organisation. Smaller teams can also leverage such a setup, and there are a couple of notable tools on the market now.

Enterprise and large tech has caught on to this need. GitHub launched GitHub Packages in 2019 as they deemed the need for private registries a sizeable enough market worthy of pursuing. GitHub offer a pay-as-you-go model for using the service, the billing of which is based on storage and data transfer. This model offers a small amount of resources for free users too, that aim to onboard them into the GitHub ecosystem.

NPMJS also offer a private registry service with a monthly subscription.

Back to developing packages

The next section will document the setup process of a TypeScript NPM package. As the majority of my articles are focused on React Native, the content here will be based on developing React Native packages, but the setup can be applied to any TypeScript project.

TypeScript NPM Package Setup

VS Code is the best TypeScript-supported editor, which is no surprise since Microsoft maintain both the editor and TypeScript itself. For this reason, VS Code will be the editor of choice we’ll focus on here.

Installing and Configuring TypeScript

TypeScript is supported out of the box in VS Code, but the compiler, tsc, needs to be added to your project as a dev dependency.

A dev (developer) dependency is a dependency that is installed on the developer’s machine, required for developing the package, but not required when importing the package code itself.

If you are developing a package from scratch, initiate your package and add typescript as a dev dependency:

yarn init
yarn add -D typescript

Verify the version of TypeScript installed in your package.json. At this time of writing, version 4.0.2 was the latest stable version:

"devDependencies": {
"typescript": "^4.0.2"
}

The tsc command will now immediately become available, enabling you to compile your TypeScript. We’ll look more at tsc further down, and how it can be automatically fix a range of errors based on pre-defined linting rules.

Upon installing TypeScript the immediate next step is to define your tsconfig.json file. This file defines compilation options including the output directory, whether type definition files should be included, module format of the compiled JavaScript, and a lot more. In fact, for a React Native project there are some specific configuration options that need to be defined within tsconfig.json.

Consider the following configuration, with the more interesting properties highlighted in bold:

// `tsconfig.json` for react native TypeScript package{
"compilerOptions": {
"target": "es6",
"module": "es6",

"declaration": true,
"outDir": "./lib",
"strict": true,
"jsx": "react-native",
"skipLibCheck": true,

},
"include": [
"src"
],
"exclude": [
"node_modules",
"**/__tests__/*"
]
}

Let’s break down some of these properties:

  • React Native modules are coded using ES6 syntax, therefore the es6 value is given to the module property. Conversely, Node.js modules conform to commonjs syntax. Since we are working with React Native, es6 can also be the output format. Remember React Native projects will be further compiled into a final build before being uploaded to an App Store.
  • The jsx property tells the TypeScript compiler how to treat the JSX present in your project at compile time. There is a react-native option built in.
  • With this setup, the compiler takes files from the src/ folder and outputs the compiled JavaScript into a lib/ folder. It is this lib/ folder that is uploaded to an NPM registry.
  • skipLibCheck set to true will bypass type checking for your imported libraries (e.g. your node modules). Turning on this option is viewed by some as decreasing the integrity of your code, but if your package relies on imports that have compile time errors due to poorly defined or missing types, then skipLibCheck will ignore these problems. The compiler will only consider your project files with skipLibCheck turned on.
  • declaration set to true allows the compiler to generate type definition files for each of the compiled JavaScript files. These files are denoted by the .d.ts suffix, and make your compiled JavaScript usable by TypeScript projects.

Concretely, the compiled JavaScript inside lib/ will be uploaded to NPM, along with the required meta files such as package.json. The src/ directory containing your TypeScript files will be ignored by NPM.

Ensuring only the lib/ folder is uploaded to NPM

We’ve just configured the TypeScript compiler to compile the package into a lib/ folder, and therefore we’re only interested in uploading that folder to the NPM registry (along with the required package.json file and other meta files).

There is a quick way to do this — by defining a files property in your package.json in conjunction with main and types properties:

  • files takes an array of globs (file paths) as its value. Only these file paths, along with some required files (package.json itself being one of them) will be uploaded to NPM.
  • main defines the index, or entry file, of the package. Since an index.js will not be found in the project root directory, we need to explicitly define where the entry file is.
  • Similar to main, types defines where the main type definition file is located, that is also in the lib/ folder.

These 3 properties can be defined as follows:

// package.json {
...
"main": "lib/index.js",
"types": "lib/index.d.ts",
files": [
"lib/**/*"
]
}

The ability to define whitelist of files in this manner is more efficient than a blacklist such as the .npmignore file, that would need to be amended every time a new file is added outside of the lib/ directory.

More information about the files property of package.json can be found here in the official NPM docs.

VS Code Extensions and TypeScript Support

To aid in your development, VS Code can also be configured to automatically include import statements as you write code pertaining to a particular library you have installed via package.json. Turning on auto imports can be found under the TypeScript settings — simply search for autoImports to summon the options:

Turning on auto imports for TypeScript and JavaScript files

Another useful setting is to automatically update import statements if a file is moved (and thus changing the relative path to the imports). Search for update imports and enable this setting:

Enabling import updates on file move for TypeScript and JavaScript files

Another option worth toggling is the quote style TypeScript setting, allowing you to define single or double quotes, or automatically derive based on the file content.

Completing function calls is also a time saving option, that can be turned on by simply searching for complete function calls in Settings.

To save this article becoming a huge list of useful TypeScript configuration options, it is worth browsing the entirety of the TypeScript extension settings and automate tasks that you feel are valuable to your workflow.

React Native TypeScript Snippets Support

Also worth mentioning here is a popular snippets extension that may be useful for the reader. The extension is called ES7 React/Redux/GraphQL/React-Native snippets and can be found here on VS marketplace.

Once installed, open a TypeScript file and press SHIFT + CMD + R (macOS) to open the snippets menu. You can then type in a snippet’s corresponding code to insert that snippet into your document. For example, typing imp will fetch an import statement. Once the snippet is added to your file, press tab to move through the placeholder text and replace with your desired values.

Here is what that imp snippet looks like:

Using code snippets in TypeScript

Committing some snippet codes to memory will speed up development, especially when inserting repetitive code blocks such as import statements.

TypeScript Linting Setup

Continuing the TypeScript environment support, we will now turn to VS Code itself and the support it offers for TypeScript linting. Firstly, ensure that you have the TS Lint extension installed. TS Lint adds a range of settings to VS Code that aid in configuring your TypeScript linting setup.

One of those tools is to automatically fix certain errors upon saving a file. To set this up, search for codeActionsOnSave from the VS Code Settings tab, and then click Edit in settings.json.

Code actions on save can be applied to a user or the currently open workspace

Replace the default null entry with the following JSON to allow auto fixing:

"editor.codeActionsOnSave": {
"source.fixAll.tslint": true
},

In general, TS Lint provides some global configurations for TypeScript linting that will be familiar from the tsconfig.json file we explored earlier.

Simply search for TS Lint from your Settings tab to bring up the extension config options.

Although adhering to these global options (relative to the user or workspace) may be useful if a workspace only consists of one TypeScript project, I am personally more comfortable working with the tsconfig.json file directly from the Terminal and providing the tsconfig.json file via the -p or --project flag with tslint.

In order for linting to work, tslint it needs rules to adhere to. TS Lint comes with some default rules termed the recommended rules. To apply these rules, create a tslint.json file within your project directory and add the following:

// tslint.json{
"extends": [
"tslint:recommended"
]
}

Check out the full list of recommended rules here on GitHub.

More rules can be added to the extends property, but the recommended rules are quite strict in and of themselves, forcing conventions such as alphabetical ordering, particular white-spacing, naming conventions, and more. If the reader is interested in exploring all the TS Linting rules, or even developing their own, start by visiting the list of core rules.

Linting and Compiling

We will explore linting and compiling in more detail further down with a dummy package, but at this point you can indeed lint and compile your TypeScript:

  • The tslint command will lint your project and flag any errors or warnings relative to your rules defined in tslint.json.
  • The tsc command will compile your project and output the compiled JavaScript in the lib/ folder.

This section has covered a lot of TypeScript configuration and setup relative to VS Code. You should now be fully prepared to develop TypeScript NPM packages, or any TypeScript project!

Continuing with our goal of Creating a TypeScript-based React Native NPM Package, the next section will look at an implementation of a package that simply hosts a styled button. We’ll look deeper into workspace setup, and how to work with package dependencies.

Setting up your Packages Workspace

Mentioned earlier was the notion of housing your packages in a standalone project vs embedding them in an existing project. This decision determines where your package source code will be located.

A standalone project is an entirely separate project directory to house your packages with its own package.json and set of dependencies. Embedding your packages in an existing project however will pertain to having a packages/ folder inside that project’s directory, and the modules of which can leverage the dependencies already installed for that project.

Weigh up which option better suits you by considering the following:

Choosing whether to have your packages embedded in an existing project or as a standalone project.

The embedded approach is interesting as any peer dependency required for your package to work will already be installed, as the packages being created will likely be targeting the app within the current workspace.

Peer dependencies are dependencies that your package requires to work, but are not included in the package’s node modules. Instead, the package assumes that the hosting project will already have these dependencies installed, and uses that version instead.

Let’s consider a scenario where you are developing your first React Native app, and the components you create for this app will eventually be abstracted into packages that other apps will be able to import and use. In such a scenario it makes sense to develop the packages in the same environment as the app source code itself.

Doing so will remove the task of setting up peer dependencies for the package environment, as they would already be installed within the app environment:

// embedded package development workspacemy-app-workspace/
package.json <- all peer dependencies satisfied as `dependencies`
packages/ <- ignore in .gitignore
Button/
package.json <- cite `peerDependencies`
lib/
src/
...
src/
...

In such a setup, the packages/ folder could also be added to a separate VS Code workspace for a dedicated packages workspace. The dependencies installed for my-app will still be available and satisfy each package’s peer dependencies.

Peer dependencies can be listed in package.json inside a peerDependencies property. react and react-native are such peer dependencies that need to be installed alongside the package if it uses components such as View, Text, ScollView, etc. Treating React Native (and other packages) as peer dependencies prevents duplicate installations and multiple versions of the same package.

Another example of a peer dependency could be @react-native-community/async-storage where your package needs to persist data on the device where the app in question does not need to know about such activity. It’s likely that this package will already be installed and used within the app itself. Adding it to peerDependencies is just like the dependencies list:

peerDependencies: {
"@react-native-community/async-storage": "~1.11.0"
}

In the event you do choose to embed your package source code within your app workspace, be sure to add the packages/ directory to .gitignore. You can even hide the directory from the side menu bar by searching for Files: Exclude in the Settings tab, and add packages/ as a pattern.

This setup choice boils down to convenience and less maintenance of peerDependencies as you are developing packages. For package libraries designed for a multitude of projects from the off, then it makes sense to create a standalone project to manage the package source code.

For the standalone project, have your package.json have installed all the peer dependencies that the packages rely on. Then in the packages directory, have separate package.json , and therefore different modules, for each of the packages:

// standalone package development workspacepackages-workspace/
package.json <- install peer dependencies as `dependencies`
packages/
Button/
package.json <- cite `peerDependencies`
lib/
src/
...
SmallButton/
package.json <- cite `peerDependencies`
lib/
src/
...
...

The above setup will ensure that VS Code will flag no errors pertaining to missing packages, while allowing you to configure the peer dependencies for each of your packages in question.

You will now know the best workspace solution to start developing your packages. We’ll next look at a simple example package where we’ll put everything together, and finally publish the package to an NPM registry.

An Example TypeScript Package: Styled Button

This section will explore a demo project I have set up for this piece that is available on GitHub.

The project represents a standalone workspace that hosts one package, @myorg/Button. This component is simply a button using React Native’s TouchableOpacity, View and Text components, in addition to some styling. Note that the scope @myorg has been used here, with the intent of grouping all your packages under that one scope. We will see that scopes speed up the process of upgrading your packages in the next section.

As the packages hosted in this project rely on React and React Native libraries, these are installed as dependencies in the top-most package.json.

Again, this setup ensures that VS Code does not flag warnings or errors of missing dependencies. Project dependencies will not be installed within the package, who only refer to such dependencies as peer dependencies.

One simple method to bootstrap a packages project with the required dependencies is to run expo init and take the React dependencies from a bare workflow project.

The packages/ folder hosts our Button package that comes with its own package.json, similar in nature to what we discussed above. The @myorg/Button package itself contains everything we have discussed in this piece so far. This is the general structure of the project:

// structure of packages projectrn-packages-demo/
packages/
Button/
src/
index.tsx
styles.ts
types.ts
package.json
tsconfig.json
tslint.json
package.json
...

Note that each package contains its own package.json, tslint.json and tsconfig.json files. When considering how to upload packages to source control, one can either set up one repository to host all the packages (almost acting like a Monorepo), or upload each package to a separate repository.

I personally have opted for the former solution when developing organisation packages in a private registry, but the latter solution will be more favoured if the package is open sourced for community contributions.

tslint.json could have been defined on the top level, but in a modular fashion, having separate linting rules for each package will cover edge cases where rule changes need to be made for individual packages.

Linting, Compiling and Publishing

Taking a look at the @myorg/Button package.json file, we can see that there are some scripts defined to aid in linting and publishing:

// package.json {
...
"scripts": {
"build": "tsc --project ./tsconfig.json",
"pub": "npm version patch && publish --registry http://localhost:4873/",
"lint": "tslint --fix -p ./tsconfig.json"
}
}

These are some basic scripts to get you started, and can be run with yarn <script_name>. Let’s break down what is happening here:

  • The build script uses tsc to compile the TypeScript in src/, outputting the compiled JavaScript in lib/ (as defined in package.json also). Note that the --project or -P flag can be used to point to a specific tsconfig.json file.
  • The pub script increments the patch version number (the final number in the Semantic Versioning convention) and then publishes the package to the local private Verdaccio registry, that by default runs on localhost at port 4873. The --registry flag can be used to point to any registry of your choosing.
  • The lint script runs tslint and analysis your TypeScript, pointing out any errors and warnings in the output relative to the rules defined in tslint.json. The --fix flag allows the compiler to fix warnings automatically where it is intelligent enough to do so. Note that all linting CLI options can be found here.

To test out the lint script, try changing the IButtonProps type to just ButtonProps, and see how the lint script responds when you run it again. Also try changing the ordering of the properties in styles.ts so they are not alphabetical to verify tslint picks up these changes.

Everything we have discussed prior to this demo applies to it, including:

  • The lib/ folder is ignored throughout the project by .gitignore to prevent the compiled JavaScript being committed to source control.
  • The files property in Button’s package.json ensures that only the lib/ folder is published to NPM, along with the required meta files.
  • peerDependencies are also defined, with their version ranges supported by the package.
  • Type definition files are generated alongside the compiled JavaScript files thanks to the declaration: true property of tsconfig.json. This will result in a styles.d.ts, index.d.ts and types.d.ts files being generated when yarn build is run, enabling TypeScript support for the package.

Although out of the scope of this piece, NPM also has built-in support for package.json scripts whereby scripts of particular names are called at various stages of the publishing pipeline. The reader can familiarise themselves with those capabilities here.

The final thing this piece will cover is installing and upgrading your packages in an app project.

Installing and Upgrading the Button package

Briefly covering the installation and upgrade process, simply install your package with yarn (or npm):

yarn add @myorg/button

If you are using a private registry like Verdaccio, make sure your NPM registry is set globally prior to attempting the install:

npm set registry http://localhost:4873/

It is common to rapidly iterate your packages as you are developing an app, especially in a private environment. To save time upgrading the packages, the scope can be utilised to upgrade every single package under that scope in your project. Simply use the --scope flag to do so:

// upgrading all packages under a scopeyarn upgrade --scope @myorg

This time saving command will bring all packages under the scope to the latest version (relative to the supported range defined in the app’s package.json).

In Summary

This piece covered all the major aspects (and lots of minor ones too!) of developing TypeScript based NPM packages for React Native. The reader should now be equipped with the tools and workflow to develop their own TypeScript packages for React Native.

The techniques discussed here are not limited to React Native. Now you know how to configure a TypeScript project in detail, the techniques discussed here can be applied to any TypeScript based project.

The demo project discussed here is available on GitHub for the reader to refer to.

Programmer and Author. Director @ JKRBInvestments.com. Creator of LearnChineseGrammar.com for iOS.

Get the Medium app