TypeScript

Summary

Bare minimum required for sharing a React component written in TypeScript.

Nobody wants to write React without JSX. As we're going to need a build step anyway and there's no sane reason to build something without TypeScript nowadays, we're going to go directly to JSX + TypeScript.

Starting from the bare example, we

  • move the index.js to src/index.tsx (to better separate code and build artefacts later), and
  • add typescript and React's types to our devDependencies (see TODO for an overview over the different dependency types): pnpm add -D typescript @types/react.

Once we set up a build step, the built library will be exposed in dist/index.js together with a declaration file at dist/index.d.ts, so we update the package.json accordingly:

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

Setting up TypeScript

In order to compile TypeScript + JSX to vanilla JavaScript, we will add a build script to our package.json that runs the TypeScript compiler (we will use fancier tooling like esbuild in the future):

{
  "scripts": {
    "build": "tsc"
  }
}

By default, the TypeScript compiler doesn't really know what to do with your stuff, so you need to create a tsconfig.json:

{
  "include": ["src"],
  "compilerOptions": {
    "target": "ESNext",
    "moduleResolution": "nodenext",
    "outDir": "dist",
    "jsx": "react-jsx"
  }
}
  • include (Reference): This tells TypeScript which files to look at.
  • compilerOptions.target (Reference): This tells TypeScript which version of ECMAScript (aka which JavaScript standard) to compile to. When developing apps, this should be set to something sensible like ES6. For libraries like here, we want the consuming application to have full control over its own bundling and polyfilling, so we use the most up-to-date standard, which is exposed as ESNext.
  • compilerOptions.moduleResolution (Reference): There's three choices here, classic, node and nodenext (aka node16). You probably never want to use classic in modern projects. node references Node's CommonJS resolution algorithm. Since we want to emit an ES Module, we will use nodenext, Node's ES Modules resolution algorithm.
  • compilerOptions.outDir (Reference): This tells TypeScript where to put the compiled files. We want them in dist (don't forget to add that directory to your .gitignore!)
  • compilerOptions.jsx (Reference): This informs the TypeScript compiler that we will use JSX syntax. TypeScript can convert that either to createElement calls through react (which we used in Level 1) or newer _jsx calls available since React 17 through react-jsx (see this blog post). We want the modern stuff and will use react-jsx.

The library can now be published and consumed analogous to the library from Level 1. However, VSCode won't be happy with us, as we did not expose any declaration file (the file telling TypeScript which vanilla JS thing has which type, see reference). In order to do that, we extend our tsconfig.json:

{
  "compilerOptions": {
    "declaration": true
  }
}

Once we run pnpm build for our library once more (and publish it if necessary), VSCode understands the type of our imported button:

VSCode understands the button's type

tsup

Compiling our 5 LOC, 1-component component library currently takes 2.4s on my machine. Once a library gets bigger, the compile time can grow significantly. Over the last years, a lot of fantastic Rust- and Go-based tooling has been developed. We're going to use tsup, which internally uses esbuild:

pnpm add -D tsup

We'll use the following tsup.config.ts config file:

import { defineConfig } from 'tsup'
 
export default defineConfig({
  entry: ['src/index.tsx'],
  dts: true,
  target: 'esnext',
  format: 'esm',
  sourcemap: true,
  minify: false
})

Update the package.json accordingly:

{
  "scripts": {
    "build": "tsup"
  }
}

Running pnpm build now takes 1.4s on my machine, and only 4ms of those are spent on actually compiling the library.

Watch mode

To avoid having to re-build the library everytime you change something, you can use the watch mode provided by tsup. Simply add a script to your package.json:

{
  "scripts": {
    "dev": "tsup --watch"
  }
}

Running pnpm dev will now re-build the library on file changes.