Create a React Native TypeScript Package

Intro

It can be tough to get a TypeScript package to work in a TypeScript React Native project. Hopefully this will allow you to avoid many of the pitfalls.

Follow these Steps to Create a Package

Firstly, make sure you have a suitable version of node installed for the React Native project that you want to target.

Now create a folder for your package and cd into it:

mkdir my-package-folder
cd my-package-folder

And then create a package.json file in this folder by typing:

npm init

Answer all the questions and you will get a package.json file populated with your choices.

Edit the package.json file leaving most of the lines alone but making the following changes (i.e. change the value for the “main” field and add the “types” field):

{
  ...
  "main": "./dist/main.js",
  "types": "./dist/main.d.ts",
  ...
}

Next create a new file called tsconfig.json in the same folder and with at least these element (explanation is later on …):

{
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs",
    "moduleResolution": "node",
    "esModuleInterop": true
  },
  "include": [
    "src/**/*.ts"
  ],
  "exclude": [
    "node_modules",
    "./dist"
  ]
}

Create a folder with the name src inside your project (at the same level as the package.json)

mkdir src

In your src folder include some TypeScript source files strictly adhering to these rules for exports from your TypeScript code. Each TypeScript source file that you want to have exports visible in the React Native project should either contain:

  • A single class or function definition marked as export default – this is the FileWithSingleClassOrFn referred to below
  • Multiple classes and/or functions marked as export (but NOT default) – this is FileWithManyExports referred to below.

Now create a main.ts file in the src folder.

The main.ts file is critical as this determines how you will access the package in your React Native project. I have found that the following forms of exporting work – while many other alternatives don’t – at least for me.

For each of these two types of file include one line in the main.ts file with the appropriate syntax.

So, for each file that corresponds to the FileWithSingleClassOrFn pattern include the line:

export { default as ExportNameWhichCanBeFileName } from './ClassOrFnAloneInAFile';

And for each file that corresponds to the FileWithManyExports pattern include the line:

export * from './FileWithSingleClassOrFn';

Build your package using npm run build-all

The easiest way to try out your package in your React Native TypeScript project is to run the following command in the package folder that we’ve just been working in:

npm link

Now go to the folder in your React Native TypeScript project that contains the package.json file and enter (where <your-package-name> is the name you entered into the package.json file for the package you created):

npm link <your-package-name>

A Deeper Dive into How I Got To This

After spending many hours (some online with Chris Greening who has been ultra helpful!) trying to decipher the inane javascript that is generated when transpiling code with the various different tsconfig settings, I ended up with a tsconfig.json and package.json which work for my situation. I hope they help save someone else some trouble!

The tsconfig.json File

The key elements of the tsconfig.json that I identified are distilled in this snippet:

{
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs",
    "moduleResolution": "node",
    "esModuleInterop": true
  },
  "include": [
    "src/**/*.ts"
  ],
  "exclude": [
    "node_modules",
    "./dist"
  ]
}

“target” really defines the expectations for the javascript engine that will run the generated code. i.e. what version of node (or browser for other projects) will be used when the generated code is executed. It is frankly pretty painful to use anything less than es6 (which is identical to es2015 by the way) as before that things like fat-arrow functions ( () => {} ) and promises were not supported well. Anything later than es6 is up to the user but make sure the React Native project that you are targeting is going to be built using a node version of 12 or higher (for es2018 for instance) – for the full story here’s an insane table of node comptibility to peruse at your leisure!

“module” defines the code that will be generated for exports from your package. Unfortunately this is where a lot of the problems arise as most (all?) React Native TypeScript projects use commonjs as their mechanism for using packages. This mechanism is really limited and has a lot of quirks. To stay away from the quirky behaviour please follow the guidelines I’ve set out for the contents of source files and the contents of the “main” file which “passes on” your packages exports to the outside world.

“moduleResolution” and “esModuleInterop” are here to ensure that weird problems don’t crop up in the use of imports. There is a full explanation of what these changes mean here-moduleResolution and here-esModuleInterop but suffice it to say that weird shit happens whether you use these options or not. The reason I’m using them is that the weirdness seems slightly less if they are present! Enough said.

The package.json File

The key role of the package.json file in defining the exports for your package are these two lines:

{
  ...
  "main": "./dist/main.js",
  "types": "./dist/main.d.ts",
  ...
}

“main” defines the file that will be inspected first when a package is included in another project. Note that when your package is used in a React Native project it will be inside the node_modules folder and the package.json needs to tell node where to find the exports. So we must place these in the main.ts files that we created earlier and this will be transpiled to ./dist/main.js if you followed the settings I’ve used when building the package.

“types” defines the exports for TypeScript so that type-checking can be done successfully on your package.