Convert flowed-project to TypeScript

Jan 23, 2019

Why Typescript?

First, I do not recommend everyone to move to TypeScript. Who’s being happy with Flow, it’s fine. I used Flow for my projects for a while and it’s still a powerful tool for type checking to prevent human mistake while developing process.

The main reason is that I want to give TypeScript a shot for my side project, it’s a trend for style checking now. TypeScript has team support behind, not like Flow and more tools or projects already integrated TypeScript nowaday: Angular, React, Vue, Babel 7, etc.

typescript trend
typescript trend

Handle Project

You can find my public project here and the pull request I made from flow-to-typescript branch as well to see what I changed clearly. I created from create-react-app (CRA) version 1.5 and I applied Flow type for it in the beginning. Now I’ll try to notes what I have done when converting my project from Flow to TypeScript.

Install TypeScript

First, we need to install typescript library and its dependencies

$ npm install --save-dev typescript @types/node @types/react @types/react-dom @types/react-router-dom @types/jest @types/moment @types/webpack-env

Configuration File

Second, we’ll create a configuration file name tsconfig.jsonfor TypeScript in our root project folder.

{
  "compilerOptions": {
    "target": "es5",                          /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
    "module": "commonjs",                     /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
    "lib": ["es2018", "dom"],                 /* Specify library files to be included in the compilation. */
    "jsx": "react",                           /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */

    /* Strict Type-Checking Options */
    "strict": false,                           /* Enable all strict type-checking options. */
    "strictNullChecks": true,                  /* Enable strict null checks. */

    /* Additional Checks */
    "noUnusedLocals": true,                    /* Report errors on unused locals. */
    "noUnusedParameters": true,                /* Report errors on unused parameters. */

    /* Module Resolution Options */
    // "moduleResolution": "node",              /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
    "baseUrl": ".",                             /* Base directory to resolve non-absolute module names. */
    "paths": {
      "@/*": ["./src/*"],
      "screens/*": ["./src/screens/*"],
      "components/*": ["./src/components/*"],
      "assets/*": ["./src/assets/*"]
    },
    "esModuleInterop": true                   /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
  },
  "exclude": ["node_modules"]
}

I already comment options in configuration files. You can find more in the official document.

Update File Names

Next stuff is we need to convert all file *.js to *.tsx for React Component (including test file for React component), or to *.ts for utility or support files.

Remove all // @flow annotations in which files we used it before.

Update Babel Configuration

{
  "presets": [
    "@babel/preset-typescript",
    "@babel/preset-env",
    "@babel/preset-react",
    "react-app",
  ],
  "plugins": [
    [
      "module-resolver", {
        "alias": {
          "@": "./src",
          "screens": "./src/screens",
          "components": "./src/components",
          "assets": "./src/assets"
        }
      }
    ]
  ]
}

Please note here, I have to update module-resolver configuration a little bit to work with import relative modules. In the past, when I worked with Flow, I only need to define module-resolver configuration such as "root": ["./src"] and it worked fine with relative path import. However, with TypeScript, I have to declare every relative path I use in project, and it have to be the same as baseUrl and paths in TypeScript configuration file above.

Adapt TypeScript Checker

From this step, everything I need to do is adapt type in my code to pass TypeScript checker.

We can add one more script, for example, typescript in package.json so that we can run TypeScript checking before we run unit test.

"scripts": {
    "precommit": "NODE_ENV=production lint-staged",
    "start": "node scripts/start.js",
    "build": "node scripts/build.js",
    "predeploy": "npm run build",
    "deploy": "aws s3 sync build/ s3://fx-position-app",
    "typescript": "tsc",
    "test": "TZ=Asia/Ho_Chi_Minh jest --testPathPattern=__tests__ --detectOpenHandles",
    "testall": "eslint . && typescript && npm run test",
    "lint": "eslint ."
  },

Notes

I’ll take note some experience when working with TypeScript the first time.

Prefer Interface to Type

According to my friend recommend, I use interface instead of type. You can research more about interface vs type topics, for example this one https://www.educba.com/typescript-type-vs-interface/.

Before

type State = {
	timeOption: string,
	finishRingBell: boolean
};

After

interface IState {
  timeOption: string
  finishRingBell: boolean
}

Moment Type

Before

// @flow
import type { Moment } from 'moment';

type State = {
  currentTime: Moment
};

After

import { Moment } from 'moment';

interface State {
  currentTime: Moment
};

Non-null assertion operator

This is a new concept when I work with TypeScript. It tells the compiler that our object is not null. Let see an example:

Before

type State = {
  lots: ?number
};

class PositionCalculator extends React.Component<{}, State> {
  state = {
    lots: null
  }
  
  calculate = () => {
    this.setState({ lots: 1 });
  }
  
  render() {
    <div>
      <button className="btn btn-primary" onClick={this.calculate}>
        Calculate
      </button>
      <div className="col-md-4">
        <h5 className="card-title">Result</h5>
        <label>Lots: {lots ? lots.toFixed(3) : 'N/A'}</label>
      </div>
    </div>
  }
}

After

interface IState = {
  lots: null | number
};

class PositionCalculator extends React.Component<{}, IState> {
  state: IState = {
    lots: null
  }
  
  calculate = () => {
    this.setState({ lots: 1 });
  }
  
  render() {
    <div>
      <button className="btn btn-primary" onClick={this.calculate}>
        Calculate
      </button>
      <div className="col-md-4">
        <h5 className="card-title">Result</h5>
        <label>Lots: {lots ? lots!.toFixed(3) : 'N/A'}</label>
      </div>
    </div>
  }
}

Notice that we update lots.toFixed(3) in Flow type to lots!.toFixed(3).

I’m a beginner TypeScript and surely I have a large room to improve. Hopefully, my very first experience with TypeScript can help someone out there who wanna give TypeScript a shot.