Let’s play nice: Typescript, JS, Webpack, Babel, React, and Relay

Become a Subscriber

I spent the better part of an afternoon trying to upgrade an React/Redux/Relay app from Javascript to Typescript. Now that babel and Typescript play nice together, I thought it would be an easy process. Turns out the configuration is easy, but it took a lot of trial and error to dial in the settings. Furthermore, there’s a lot of really bad advice out there complete with bandaids and hacks that make the process more convoluted than it should be. I’m here to help. Let’s do this!

Let’s start with the packages you’ll need in your package.json. You’ll need the typical dependencies for React/Relay as described in their own Installation Guide. Then as devDependencies you’ll want the following packages:

{
"devDependencies": {
    "@babel/core": "^7.0.0-beta.42",
    "@babel/preset-env": "^7.0.0-beta.42",
    "@babel/preset-react": "^7.0.0-beta.42",
    "@babel/preset-typescript": "^7.0.0-beta.42",
    "awesome-typescript-loader": "^5.0.0-1",
    "babel-loader": "^8.0.0-beta",
    "babel-plugin-relay": "^1.5.0",
    "relay-compiler": "^1.5.0",
    "sass-loader": "^6.0.7",
    "style-loader": "^0.20.3",
    "typescript": "^2.7.2",
    "webpack": "^4.2.0",
    "webpack-cli": "^2.0.12",
    "webpack-serve": "^0.2.0"
  }

I kept this package list as short as possible. It’s a personal pet-peeve of mine when developers throw in packages in a cavalier style; turning their apps into bloatware.

Setup Typescript

I’ll give you the source of my tsconfig.json and then break down the explanations:

{
  "compilerOptions": {
    "allowJs": true,
    "baseUrl": "./src",
    "rootDir": "./src",
    "jsx": "react",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "pretty": true,
    "sourceMap": true,
    "moduleResolution": "node",
    "target": "es6",
    "lib": ["es7", "dom"]
  },
  "include": ["./src/**/*"],
  "exclude": ["node_modules"]
}

Options here such as pretty and sourceMap aren’t necessary to get this working. However, I know what it’s like to be a beginner and have some senior developer overlook all the assumptions. This config should get everything to play nice for your app and solve the core problem of transpiling chains.

The really import bits are:

  • allowJs so that we have interoperability between TS and JS
  • target being es6 so that we can first transpile down one step, before handing over to babel
  • lib having es7 and dom so that this is fairly future-proof while still being backwards compatible.

Setup Babel

Typescript will be the first transpiler in the chain. Since it’s a superset of Javascript, it’s fairly straightforward and easy to setup. However, understanding the order of transpilation and config involved in each step is a big disconnect for most developers. I struggled with this myself for some time before taking the time to actually learn what was going on. Here’s what you’ll want in your .babelrc file:

{
  "presets": [
    "@babel/typescript",
    "@babel/react",
    "@babel/env",
  ],
  "plugins": [
    "relay"
  ]

}

Everything here should be pretty straightforward. We listed the presets and relay plugin in our dependencies. The order here matters again as we’re transpiling from Typscript down to es5.

Setting up Webpack

Getting webpack working with this build process is actually next to nothing. But I don’t want to overlook any steps or make assumptions. Here’s the config I’m using in a few apps:

const { CheckerPlugin } = require('awesome-typescript-loader')
const path = require('path')

module.exports = {
  entry: path.resolve(__dirname, './src/index.tsx'),
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, './build'),
  },

  // Enable sourcemaps for debugging webpack's output.
  devtool: 'source-map',

  resolve: {
    extensions: ['.ts', '.tsx', '.js'],
    modules: [
      path.resolve(__dirname, './src'),
      path.resolve(__dirname, './node_modules'),
    ],
  },

  module: {
    rules: [
      // All files with a '.ts' or '.tsx' extension will be handled by 'awesome-typescript-loader'.
      {
        test: /\.ts|\.tsx|\.js|\.jsx$/,
        exclude: path.resolve(__dirname, './node_modules'),
        use: ['babel-loader', 'awesome-typescript-loader'],
      }
    ]
  },

  plugins: [
    new CheckerPlugin()
  ]
}

Package Scripts to get it all fired up

Let’s bring it all together. I’m using webpack-serve here (which if you didn’t know, is the modern replacement to webpack-dev-server) to run the app. The NODE_PATH var isn’t necessary if you’re using relative paths, but in general, I like to always be explicit and have absolute imports from that root. The rest should look like a typical setup. Notice we’ve added the extensions flag to relay.

{
  "scripts": {
    "start": "NODE_PATH=./src webpack-serve",
    "relay:build": "relay-compiler --src ./src --schema ./schema.json --extensions ts tsx js",
    "relay:watch": "yarn relay:build --watch",
    "webpack:build": "NODE_PATH=./src webpack -p"
  },
}

Wrapping Up

That should be everything you need to get up and running. You should be able to upgrade any existing javascript application and slowly start transitioning to Typescript without breaking your current config. I’ve done this on three different projects that have entirely different stacks and tools, and everything is working great. Let me know if this was helpful or if you run into errors.

Happy coding!