Skip to content

Instantly share code, notes, and snippets.

@xiaoyunyang
Last active April 16, 2022 14:02
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save xiaoyunyang/277a391fac65d04039cdb250de20cb4d to your computer and use it in GitHub Desktop.
Save xiaoyunyang/277a391fac65d04039cdb250de20cb4d to your computer and use it in GitHub Desktop.
A guide for how to migrate your project from Flow to TypeScript

Flow to TypeScript Migration

Helpful Migration Guides:

Phase 1: Configure Project to use TypeScript

Delete Flow-related files

This include, but are not limited to:

  • flow-typed folder
  • flowconfig
  • flow-typed in eslintignore

Update loader

Option 1 - Delete .babelrc and babel dependencies

Why? TypeScript now supports transpiling JavaScript.

yarn remove eslint-loader
yarn remove babel-loader
yarn add --dev awesome-typescript-loader source-map-loader typings-for-css-modules-loader

Option 2 - Update babel loader

If you have a project that needs to support both JS and TS, update babel

rm .babelrc
touch babel.config.js
// babel.config.js

module.exports = function babelConfig (api) {
    api.cache(true);
    return {
        presets: [
            "@smartling/babel-preset-smartling",
            "@babel/preset-typescript"
        ],
        plugins: [
            "@babel/plugin-transform-modules-commonjs"
        ],
        env: {
            test: {
                plugins: ["@babel/plugin-transform-modules-commonjs"]
            }
        }
    };
};

Update Webpack Config to use new loader

Assuming we went with babel-loader:

/* eslint-disable @typescript-eslint/no-var-requires */
const path = require("path");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const webpack = require("webpack");

const isDevelopmentMode = process.env.NODE_ENV === "development";

module.exports = {
    bail: !isDevelopmentMode,
    cache: true,
    devtool: isDevelopmentMode ? "eval-source-map" : false,
    mode: isDevelopmentMode ? "development" : "production",
    module: {
        rules: [
            {
                test: /\.(t|j)sx?$/,
                exclude: /node_modules/,
                loader: "eslint-loader",
                enforce: "pre"
            },
            {
                test: /\.(t|j)sx?$/,
                exclude: /node_modules/,
                loader: "babel-loader",
                query: {
                    cacheDirectory: true
                }
            },
            {
                test: /\.(woff2?|ttf|eot|svg)(\?[\s\S]+)?$/,
                loader: "url-loader",
                options: {
                    name: "[name]-[hash].[ext]"
                }
            }
        ]
    },
    plugins: [
        new webpack.DefinePlugin({
            "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV)
        }),
        new MiniCssExtractPlugin({ filename: "editor.css", allChunks: true })
    ],
    resolve: {
        extensions: [".js", ".jsx", ".ts", ".tsx", ".d.ts", ".scss", ".json", ".css"],
        modules: [
            path.resolve(__dirname, "../src"),
            path.resolve(__dirname, "../node_modules")
        ]
    }
};

After updating the webpack config, build the project (or run webpack) to generate the .d.ts files for your css / sass / pcss. This will ensure that these modules can be imported into your TypeScript project.

For more on TypeScript + WebPack + Sass

Add packages:

yarn add --dev typescript @types/react @types/react-dom @types/jest @types/enzyme @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-import-resolver-typescript

Add the TS version of all your library code

yarn add @types/deep-freeze --dev
yarn add @types/chai --dev
yarn add @types/classnames

etc etc

Add type-check script in package.json

scripts: {
 "type-check": "tsc"
}

Add TypeScript for Storybook

Guides

Add all the dependencies

yarn add -D @storybook/react @storybook/addon-info @storybook/addon-jest @storybook/addon-knobs @storybook/addon-options @storybook/addons @storybook/react storybook-addon-jsx @types/react babel-core typescript awesome-typescript-loader react-docgen-typescript-webpack-plugin jest @types/jest ts-jest

Create a .storybook directory at the root of your project

mkdir .storybook
touch .storybook/config.js .storybook/addons.js .storybook/webpack.config.js 

Add tsconfig.json:

{
   "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": false,

       // Disallow features that require cross-file information for emit.
       "isolatedModules": true,

       // Import non-ES modules as default imports.
       "esModuleInterop": true,

       "jsx": "react",

       "module": "esNext"

   },
   "include": [
       "src/**/*.ts",
       "src/**/*.tsx"
   ],
   "exclude": [
       "src/**/*.js"
   ]
}

Update eslintrc.js

//eslintrc.js
module.exports = {
    extends: [
        "plugin:@typescript-eslint/recommended",
        "eslint:recommended",
        "@smartling/eslint-config-smartling"
    ],
    parser: "@typescript-eslint/parser",
    plugins: ["import", "@typescript-eslint"],
    parserOptions: {
        ecmaFeatures: {
            jsx: true
        },
        sourceType: "module",
        useJSXTextNode: true,
        project: "./tsconfig.json"
    },
    rules: {
        "@typescript-eslint/explicit-function-return-type": "off",
        "@typescript-eslint/explicit-member-accessibility": "off",
        "@typescript-eslint/member-delimiter-style": [2],
        "import/extensions": [1, "never", { ts: "never", "json": "always" }],
        "react/jsx-indent": [0],
        "react/jsx-indent-props": [0],
        "indent": [0]
    },
    overrides: [
        {
            files: ["**/*.ts", "**/*.tsx"],
            rules: {
                "semi": 1,
                "no-unused-vars": ["off"],
                "quote-props": ["error", "as-needed"]
            }
        }
    ],
    settings: {
        "import/resolver": {
            node: {
                extensions: [".js", ".jsx", ".ts", ".tsx"]
            },
            "eslint-import-resolver-typescript": true
        },
        "import/parsers": {
            "@typescript-eslint/parser": [".ts", ".tsx"]
        },
        react: {
            version: "detect"
        }
    },
    env: {
        jest: true,
        browser: true
    }
};

I suggest excluding plugin:@typescript-eslint/recommended for a big project at first to facilitate an incremental migration.

TypeScript VSCode Integration

Open VSCode setting (See more about setting up TSLint at http://artsy.github.io/blog/2019/01/29/from-tslint-to-eslint/ and https://dev.to/dorshinar/linting-your-reacttypescript-project-with-eslint-and-prettier-8hb)

{
    "eslint.autoFixOnSave": true,
    "javascript.validate.enable": false,
    "editor.minimap.enabled": false,
    "git.enableSmartCommit": true,
    "window.zoomLevel": 1,
    "workbench.activityBar.visible": true,
    "javascript.updateImportsOnFileMove.enabled": "always",
    "typescript.implementationsCodeLens.enabled": true,
    "editor.formatOnSave": true,
    "eslint.validate": [
        {
            "language": "javascript",
            "autoFix": true
        },
        {
            "language": "javascriptreact",
            "autoFix": true
        },
        {
            "language": "typescript",
            "autoFix": true
        },
        {
            "language": "typescriptreact",
            "autoFix": true
        }
    ]
}

Phase 2 TypeScriptify Flow Project

Change all .js -> .ts or .tsx. I wrote a Bash script

$ sudo touch migrate.sh
$ vim migrate.sh
#!/bin/bash

cd $1

for f in `find . -type f -name '*.js'`;
do
   mv -- "$f" "${f%.js}.ts"
done

Then provide the directory that you want to migrate as first argument of and execute the script. $ chmod +x migrate.sh $ ./migrate.sh ~/tinext-editor/src

Delete all instances of // @flow and update import. Also, delete all instances of // $FlowFixMe

// Flow
import type { Type1, Type2 } from ./dir/to/path
import { type Type3 } from ./dir/to/path

// Typescript
import { Type1, Type2 } from ./dir/to/path

Update React components Overview

Check out

Analogues

When in doubt, try things out in TypeScript Playground or repl

This is helpful if you are migrating from Flow to TypeScript: https://github.com/niieani/typescript-vs-flowtype

Typing an object

In flow, we use type.

In TypeScript, use interface. Interface types offer more capabilities they are generally preferred to type aliases. For instance:

  • An interface can be named in an extends or implements clause, but a type alias for an object type literal cannot.
  • An interface can have multiple merged declarations, but a type alias for an object type literal cannot.

Casting

// Flow

type User = {
    firstName: string,
    lastName: string,
    email: string
}
const user = {
    firstName: "Jane",
    lastname: "Doe",
    email: "example@example.com",
    paidUser: true
};
const user2 = {
    firstName: "Jane",
    lastname: "Doe"
};

const userAsUser: User = ((user: User): User);
const user2AsUser: User = ((user2: User): User); // Fails

The casting for user2AsUser fails with the following error from flow:

Cannot cast user2 to User because property email is missing in object literal but exists in User

// TypeScript

interface User {
  firstName: string;
  lastName: string;
  email: string;
}

const user: User = {
    firstName: "Jane",
    lastName: "Jane Doe",
} as any as User;

Readonly property

In flow, Plus sign in front of property Means it’s read-only https://stackoverflow.com/questions/46338710/flow-type-what-does-the-symbol-mean-in-front-a-property

In Typescript, use the “readonly” keyword https://mariusschulz.com/blog/read-only-properties-in-typescript

Generics

In Flow

type MyList = {
  filter: Array<*> => Array<*>,
  head: Array<*> => *
}

In TypeScript

class List<T> {
  filter: T[] => T[];
  head: T[] => T;
}

For more on Generics in TypeScript: https://www.typescriptlang.org/docs/handbook/generics.html

Type extension

interface Entry {
    name: string;
    id: string;
}

interface EntryWithData<T> extends Entry {
    data?: T[];
    lastUpdated?: Date;
}

const stuff: EntryWithData<number> = {
    data: [1,2,3] // TS error: name and id are required for EntryWithData
}

Array Types

In flow, we use Array<ObjectType>

In TypeScript, we use ObjectType[]

Enum

There's no analog of enum in Flow. Enum in TypeScript allow us to make a collection of constants as types. Some examples

Combining two enums

enum Mammal {
  DOG = "DOG",
  HORSE = "HORSE",
  HUMAN = "HUMAN"
}
enum Insect {
  ANT = "ANT",
  BEE = "BEE",
  FLY = "FLY"
}

const Animal = {
  ...Mammal,
  ...Insect
}

type AnimalT = Mammal & Insect

The naming convention for enums

Use a singular name for most Enum types, but use a plural name for Enum types that are bit fields.

Extending String Based Enums

microsoft/TypeScript#17592

Use union type

const enum BasicEvents {
  Start = "Start",
  Finish = "Finish"
}
const enum AdvEvents {
  Pause = "Pause",
  Resume = "Resume"
}

type Events = BasicEvents | AdvEvents;

let e: Events = AdvEvents.Pause;
Check for membership
const animals: AnimalT[] = [“DOG”, “ANT”, “HUMAN”, “BEE”]
const mammals: Mammal[] = animals.filter(animal => animal in Mammal)
console.log(mammals) //> [“DOG”, “HUMAN"] 

Note the difference between Animal and AnimalT!

Dictionary typing using enum
enum Var {
    X = "x",
    Y = "y",
}

type Dict = { [var in Var]: string };

This is a lot simpler than Flow. In Flow, we had to do something like this:

const Vars = {
  X: "x",
  Y: "y"
}

type VarType = $Keys<typeof Vars>
type Dict = { [var in VarType]: string }

Exclude

interface Animal {
    LION = "LION",
    PIG = "PIG",
    COW = "COW",
    ANT = "ANT"
}

type DomesticatedMammals = {
    [animal in Exclude<Animal, Animal.ANT>]: boolean
}

Using enums in Map

enum One {
  A = "A",
  B = "B",
  C = "C"
}

enum Two {
  D = "D",
  E = "E",
  F = "F"
}

const map = new Map<string, One | Two>([
    ["a", One.A],
    ["d", Two.D]
])

Shape

$Shape<SomeObjectType> in Flow is analogous to creating Generics or other types to extend in typescript.

Gotchas

Tuple Types

TypeScript sometimes does not recognize Tuple Types. Solution: explicit casting or typing when declaring new var

Example

type Interval = [number, number]

const getMaxAndMin = (interval: Interval) => ({
    min: interval[0],
    max: interval[1]
});

const interval = [0, 3];

getMaxAndMin(interval);

TypeScript complains:

Argument of type 'number[]' is not assignable to parameter of type '[number, number]'. Type 'number[]' is missing the following properties from type '[number, number]'

Solution:

const interval: Interval = [0, 3];

getMaxAndMin(interval);

Or

getMaxAndMin([0,3]);

Enzyme Mount

class  MyButton extends React.Component {
    constructor() {
        this.handleClickBound = handleClick.bind(this);
    }
    handleClick() {
        console.log("do something");
    }
    render() {
        return (
            <button onClick={handleClickBound}>Click Me</button>
        )
    }
}
const button = mount(<MyButton />);
const buttonInstance = button.instance();
buttonInstance.handleClick()

Property 'handleClick' does not exist on type 'Component<{}, {}, any>'

Solution:

const button = mount<MyButton>(<MyButton />);

Phase 3 Fix Types, Interfaces, and Missing Properties

Fix Missing Types and Missing Properties

  • Fix all lint errors
  • Fix any types

Phase 4 Regression Testing

  • Re-run unit tests to make sure they all pass
  • Integration testing - if the project is a library, make sure you can build it and integrate it into a project that's not TypeScript.

Optional:

  • Update your CI/CD pipeline to include type checking as part of the build process. For example
// Jenkinsfile
stage('Type Check') {
    sh 'yarn tsc'
}

TypeScript Learning Resource

TypeScript Learning Resources

@DmitryMasley
Copy link

Actually no need to remove babel, as it not only remove types from code, it also converts ES syntax to older one for browser compatibility. Changing babel to some other plugin you have risk of regression.

@DmitryMasley
Copy link

Also babel-loader omits typecheck, that is much faster. You can run typecheck in IDE and in build pipline, no need to check type on every build.

@DmitryMasley
Copy link

I don't understand why we setup TSLint here. We using integration with ESLint.

@DmitryMasley
Copy link

Need to remove FlowStatusWebpackPlugin, unless you want to support both typescript and flow files.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment