React Custom Toolchain Setup: TypeScript

React Custom Toolchain Setup: TypeScript

As promised in my previous blog React Custom Toolchain Setup I have added typescript to the setup, though with slight twist.

I started from same base again by cloning https://github.com/paradoxinversion/creating-a-react-app-from-scratch

Upgrade to latest

Then upgraded the dependencies to latest by

yarn upgrade --latest

This left me with one issue with webpack development server start script. Updated it to suite version 5

"scripts": {
    "start": "webpack serve",
}

Ref: https://github.com/bmhaskar/creating-a-react-app-from-scratch/commit/a97042584bbefc8387e9a7f449a68f391a507f6b

Add typescript

At this point we are already using babel as transpiler with webpack.

There were multiple approaches to introduce the typescript

  • Add a different loader like ts-loader or awesome-typescript-loader and run entire transcompilation through it, remove babel
  • Chain multiple loaders like ts-loader and babel-loader manually
  • Use babel-loader with @babel/preset-typescript which allows for transpiling the TypeScript code into JavaScript and then shipping it to babel's pipeline.

I have taken approach third approach for simple reasons

  • Babel is super configurable . Checkout https://github.com/babel/awesome-babel A list of awesome Babel plugins, presets, etc. to explore possibilities with Babel
  • It's easier to manage one compiler
  • It's faster to compile as type-checking doesn't happen at the time of transpilation
  • Since type checking is separate step, you could do it when you are ready. Helps in refactors or when you are trying things out.

So lets dive in changes

Add @babel/preset-typescript and related dependencies

yarn add -D @babel/preset-typescript @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators @babel/plugin-proposal-object-rest-spread

Add typescript

yarn add -D typescript fork-ts-checker-webpack-plugin

Update .babelrc as follows

{
  "presets": [
    ["@babel/env", { "targets": { "browsers": "last 2 versions" } }],
    "@babel/preset-typescript",
    "@babel/preset-react"
  ],
  "plugins": [
    ["@babel/plugin-proposal-decorators", { "legacy": true }],
    ["@babel/plugin-proposal-class-properties", { "loose": true }],
    "@babel/plugin-proposal-object-rest-spread",
    "react-hot-loader/babel"
  ]
}

TypeScript has a couple of extra features which Babel needs to know about (via @babel/plugin-proposal-decorator, @babel/plugin-proposal-class-properties plugins listed above).

Add tsconfig.json file to configure TypeScript type checking

{
    "compilerOptions": {
        // Target latest version of ECMAScript.
        "target": "esnext",
        // Search under node_modules for non-relative imports.
        "moduleResolution": "node",
        // Process & infer types from .js files.
        "allowJs": true,
        // Don't emit; allow Babel to transform files.
        "noEmit": true,
        // Enable strictest settings like strictNullChecks & noImplicitAny.
        "strict": true,
        // Disallow features that require cross-file information for emit.
        "isolatedModules": true,
        // Import non-ES modules as default imports.
        "esModuleInterop": true,
        // For transpiling JSX
        "jsx": "react",    
    },
    "include": [
        "src"
    ]
}

Add a script to run type checking into package.json

"scripts": {
    "start": "webpack serve",
    "check-types": "tsc"
  },

This will help you check types by running yarn run check-types (watch mode: yarn run check-types -- --watch) and ensure TypeScript is happy with your code.

Lets configure webpack via following webpack.config.js

const path = require("path");
const webpack = require("webpack");
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');

module.exports = {
  entry: "./src/index.tsx",
  mode: "development",
  module: {
    rules: [
      {
        test: /\.(j|t)sx?$/,
        exclude: /(node_modules|bower_components)/,
        loader: "babel-loader",
        options: {
          cacheDirectory: true,
        }        
      },
      {
        test: /\.css$/,
        use: ["style-loader", "css-loader"]
      },

      {
        test: /\.(png|jpg|gif)$/i,
        dependency: { not: ['url'] }, 
        type: 'asset/resource'
      },
    ]
  },
  resolve: { extensions: ["*", ".js", ".jsx",".ts", ".tsx"] },
  output: {
    path: path.resolve(__dirname, "dist/"),
    publicPath: "/dist/",
    filename: "bundle.js"
  },
  devtool:"cheap-module-source-map",
  devServer: {
    contentBase: path.join(__dirname, "public/"),
    port: 3000,
    publicPath: "http://localhost:3000/dist/",
    hotOnly: true
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin(), 
    new ForkTsCheckerWebpackPlugin()
  ]
};

Allow me to explain

It's a Webpack plugin that runs TypeScript type checker on a separate process.

const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');

// and

...
plugins: [
    ... 
    new ForkTsCheckerWebpackPlugin()
  ]
...

Following lines make sure that all js, tsx, jsx files are transpiled by babel-loader with the help of presets and plugins mentioned in .babelrc

...
 module: {
    rules: [
      {
        test: /\.(j|t)sx?$/,
        exclude: /(node_modules|bower_components)/,
        loader: "babel-loader",
        options: {
          cacheDirectory: true,
        }        
      },
            ...
        ]
}
...
resolve: { extensions: ["*", ".js", ".jsx",".ts", ".tsx"] },
...

Webpack 5 provides Asset Modules for the static assets like icons, images etc. without configuring additional loaders.


...
 module: {
    rules: [
            ...
            {
        test: /\.(png|jpg|gif)$/i,
        dependency: { not: ['url'] }, 
        type: 'asset/resource'
      },
      ...
    ]
        ...
 }
...

Configuring source maps for debugging purpose can be customized as per need from here

...
devtool:"cheap-module-source-map",
...

Lets continue with setup

Add global.d.ts file to declare any global types e.g.

declare module '*.png' {
    const content: string
    export default content
  }

This allows for loading the png files with the help of import as shown below. Above shown webpack.config.js allows us to get the path for same.

...
import image from './React-and-typescript.png';
...

const App: FC = () => {    
  return (
      <div className="App">
        <h1> Hello, World! with typescript </h1>
        <img src={image} />
      </div>
    );  
}

Last thing would be to add types. Add types by installing them

yarn add -D @babel/preset-typescript @types/react @types/react-dom @types/react-hot-loader

Last thing remaining is rename all your files which have JSX to .tsx.

For all above changes you can refer the commit https://github.com/bmhaskar/creating-a-react-app-from-scratch/pull/3/commits/db5e17fadf9b6b22e31991a0e83dc76db92fab19

All set and done!

You should be able to run your development environment with hot module replacement by

yarn start

Summary

You went through the Babel and Webpack based tooling for TypeScript code base. Current setup allows you to do development and provides robust, extensible development environment.

There are few other things like Build Generation and Testing which I will be covering in next blog stay tune.