Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save parkerproject/2b5abbc63d94598c68e8a4a759973744 to your computer and use it in GitHub Desktop.
Save parkerproject/2b5abbc63d94598c68e8a4a759973744 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

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