Tom Bell

seasoned code hacker and newbie dj

TypeScript and Preact

Last year Martin introduced me to TypeScript properly while we were working on an old project. The simplicity of a single dependency (TypeScript) vs. Babel and a number of plugin dependencies to compile for the browser is a big factor for me.

Type safety is definitely another selling point and the ability to KISS with what you use, allows you to not do anything crazy.

Until recently I didn’t have a reason to use TypeScript and React in a project. I began working on a project to better share my tracklists from DJ’ing and eventually get more statistics around those tracks played.

These tracklists are exported from Serato as CSV files, which I import into a database using another tool in the project, and served by an API for a front end to consume.

I’ve used React before, however knowing about Preact I wanted to try out a smaller alternative to React. Using the previous TypeScript and React project I worked on as a base, I started figuring out how to switch out React for Preact, and how to structure the application.

Structure

After spending a week figuring out things, and a lot of trial and error, I finally came upon a project structure, and simple set of build tools for the project. While I’m happy with the structure I’ve built for myself right now, I can appreciate it may still evolve in the future based on future findings.

TypeScript

The TypeScript config is trivial, apart from using the paths option for making type definitions from nano-css work correctly. The module option is omitted, because it defaults to es6 when the target is es5.

I prefer to enable strict, noUnusedLocals, and noUnusedParameters to make sure everything is properly type checked, and I’m not doing anything implicitly.

{
  "compilerOptions": {
    "target": "es5",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "jsx": "react",
    "jsxFactory": "h",
    "baseUrl": ".",
    "paths": {
      "nano-css/*": ["./node_modules/nano-css/"]
    }
  }
}

The use of the baseUrl and paths options will be further explained in the section about nano-css.

Preact

The structure I came up with for using Preact is having src/index.tsx as the entry point of the application. With the entry point just importing the main App component and calling render().

import { h, render } from 'preact';
import App from './components/App';

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

The App component is what contains the main Router configuration for the different pages I want to render on different paths.

It also defines the shared styling, and components for every page (namely header/navigation). I utilise nano-css for styling, because it’s a minimal dependency, and I don’t need to use CSS modules, which were a hassle to try and get working with ava for testing.

Due to some difficulties getting the nano-css type definitions working, I had to add the baseUrl and paths to the tsconfig.json.

Rollup

Instead of the complexity of a Webpack configuration file, I decided upon Rollup because the configuration is very simple to begin with. The composition of building up the plugin list feels more natural than the way loaders are configured in Webpack.

import commonjs from 'rollup-plugin-commonjs';
import resolve from 'rollup-plugin-node-resolve';
import typescript from 'rollup-plugin-typescript2';
import { uglify } from 'rollup-plugin-uglify';

const production = process.env.BUILD === 'production';

export default {
  input: 'src/index.tsx',
  output: {
    file: 'public/app.min.js',
    format: 'iife',
  },
  treeshake: true,
  plugins: [
    commonjs(),
    resolve(),
    typescript(),
    production && uglify(),
  ],
};

The rollup-plugin-typescript2 plugin is being used because the rollup-plugin-typescript plugin doesn’t do type checking during the build.

I specify the environment variable BUILD when I wish to build for production, which tells rollup to minify the JavaScript using uglify.

ESLint

Previously I used TSLint for linting, but with the announcement of The future of TypeScript on ESLint, I decided to check out typescript-eslint and eslint-config-airbnb-typescript.

module.exports = {
  extends: ['airbnb-typescript'],
  env: {
    browser: true,
  },
  settings: {
    react: { pragma: 'h' },
  },
  rules: {
    '@typescript-eslint/no-unused-vars': 'warn',
    'no-unused-vars': 'off',
    'react/no-unknown-property': ['error', { ignore: 'class' }],
  },
};

The react/no-unknown-property rule is overridden to allow the property class, because Preact allows it as well as className. The built in no-unused-vars rule is disabled, and the @typescript-eslint/no-unused-vars one enabled, because there were false positives when importing type definitions from modules.

Another one of the benefits of this switch was that the tslint-config-airbnb also included some configurations that made stylistic choices that did not line up with the original Airbnb JavaScript styleguide.

AVA

Initially I wasn’t that interested in getting a setup for testing into the project. I remember using AVA on a work related project, and I liked the simplicity of writing tests and assertions. AVA has a good recipe for setting up TypeScript, which utilises ts-node.

"ava": {
  "compileEnhancements": false,
  "extensions": [
    "ts",
    "tsx"
  ],
  "require": [
    "ts-node/register",
    "./test/helpers/setup-env"
  ]
},

For testing Preact, you need to mock the DOM. Luckily the author of Preact also made a tiny library undom for this. I made a test/helpers/setup-env.js script that I require in the AVA configuration, as seen above.

import 'undom/register';
import { config } from 'preact-render-spy';

config.createFragment = () => document.createElement('body');

document.getElementsByTagName = () => [];

The document.getElementsByTagName = () => []; exists solely to stub the function so that nano-css can load and run properly in the test environment, since it expects to be in a browser.

For rendering Preact components and checking how they render, there is an enzyme-like library preact-render-spy.

test('renders header component', async (t) => {
  const ctx = shallow(<Header />);
  t.is(ctx.find('h1').text(), 'Memoir', 'header text is correct');
});

Conclusion

Ultimately my goal was to create a single-page application front end for an API I am creating, and as a fan of simplicity I wanted to come up with some reusable patterns for future projects.

There was a fair amount of trial and error involved trying to get different libraries working together. Some of the failed attempts involved using postcss for CSS modules, but I couldn’t easily get the imports working with AVA for testing. Finally landed on using nano-css, which I personally think it’s nicer and more extensible for CSS-in-JS using it’s add-on system.