Flow gets less and less library support, while typescript seems like the go-to choice for newer projects. This documents the migration process I used to convert a React web app.
npm install --no-save @khanacademy/flow-to-ts
This tool contains some extra goodies as well as it messes up formatting less compared to the alternative setup
npm install -g @babel/cli @babel/core
npm install --no-save @zxbodya/babel-plugin-flow-to-typescript
# install parallel
brew install parallel
# or
sudo apt isntall parallel
Actually there are more plugin options. Choose the one that's most updated.
The base package: babel-plugin-flow-to-typescript
, but more up-to date alternatives atm:
@steelbrain/babel-plugin-flow-to-typescript
or @zxbodya/babel-plugin-flow-to-typescript
Warning!!! Babel configs in the project might mess with the conversion. It's a good idea, to create a secondary config with just the plugin - .babelrc.flow2ts.json
{
"plugins": ["@zxbodya/babel-plugin-flow-to-typescript"]
}
First, review the files that are going to be transformed:
Convert these to tsx:
grep -iRl --include=\*.js "@flow" ./app | tr '\n' '\0' | xargs -0 grep -l "React"
Convert these to ts:
grep -iRl --include=\*.js "@flow" ./app | tr '\n' '\0' | xargs -0 grep -L "React"
where ./app
contains the source code of the application. If you have more folders, just append after ./app
. Also if you have more extension (like jsx, add --include=\*.jsx
to the grep param)
Review files that are not going to be converted:
grep -iRL --include=\*.js "@flow" ./app
It might be a good idea to also convert these to typescript, but there might be more fixes necessary after the conversion. With javascript interop these should still work, but it will limit typescript's ability to check things.
Test the babel conversion process on a single file, to see if it works as expected:
npx flow-to-ts ./app/test_file.js --write
Or alternatively
babel --config-file ./.babelrc.flow2ts.json ./app/test_file.js -o ./app/test_file.tsx
Git won't be able to detect moving files, if they are changed/transpiled in the same commit. The best approach I found was to:
- move all files using
git mv
from.js
to.ts(x)
- commit
- move all files back that need to be transpiled
- transpile all files (see below)
- fix issues/formatting
- commit
# move files
grep -iRl --include=\*.js "@flow" ./app | tr '\n' '\0' | xargs -0 grep -l "React" | xargs -n1 sh -c 'git mv "$0" "${0%.js}.tsx"'
grep -iRl --include=\*.js "@flow" ./app | tr '\n' '\0' | xargs -0 grep -L "React" | xargs -n1 sh -c 'git mv "$0" "${0%.js}.ts"'
git commit
# move files back
grep -iRl --include=\*.tsx "@flow" ./app | xargs -n1 sh -c 'mv "$0" "${0%.tsx}.js"'
grep -iRl --include=\*.ts "@flow" ./app | xargs -n1 sh -c 'mv "$0" "${0%.ts}.js"'
#continue below
Use the grep lines above to get a list of files to convert =>
SELECT_SCRIPT | parallel "npx flow-to-ts {} --write --delete-source"
flow-to-ts will automatically detect if it should be moved to ts or tsx.
Alternatively
SELECT_SCRIPT | parallel "babel {} -o {.}.tsx --config-file ./.babelrc.flow2ts.json && rm {}"
# or just to ts
SELECT_SCRIPT | parallel "babel {} -o {.}.ts --config-file ./.babelrc.flow2ts.json && rm {}"
Use the grep commands for the files not converted, or just
# files using React
grep -iRL --include=\*.js "@flow" ./app | tr '\n' '\0' | xargs -0 grep -l "React" | xargs -n1 sh -c 'git mv "$0" "${0%.js}.tsx"'
# files not using react
grep -iRL --include=\*.js "@flow" ./app | tr '\n' '\0' | xargs -0 grep -L "React" | xargs -n1 sh -c 'git mv "$0" "${0%.js}.ts"'
but if you ran the convertion scripts even before this, you can skip the check for flow:
#files using react
grep -iRl --include=\*.js "React" ./app | xargs -n1 sh -c 'git mv "$0" "${0%.js}.tsx"'
# files not using react
grep -iRL --include=\*.js "React" ./app | xargs -n1 sh -c 'git mv "$0" "${0%.js}.ts"'
At this point formatting is probably messed up in the converted files ☹ One way to fix it if there is prettier in place after staging all files.
It's a good idea to configure prettier and eslint before doing this. See below.
npm i --no-save pretty-quick
npx pretty-quick --staged
Install preset
npm i --save-dev @babel/preset-typescript
npm i --save typescript ts-node
{
resolve: {
extensions: [".ts", ".tsx", ".js", ".json"],
}
}
{
presets: [
"@babel/preset-env",
"@babel/preset-react",
["@babel/preset-typescript", { allExtensions: true, isTSX: true }]
]
}
npm i --saved-dev @typescript-eslint/eslint-plugin @typescript-eslint/parser
Make sure you have up-to-date eslint-config-prettier and eslint-plugin-prettier configs.
{
parser: "@typescript-eslint/parser",
"extends": [
...
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"prettier",
...
"prettier/@typescript-eslint"
]
plugins: [...'@typescript-eslint'],
parser: '@typescript-eslint/parser',
settings: {
'import/resolver': {
node: {
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
},
},
}
}
{
"parser": "typescript"
}
tsconfig.json, assuming you have global imports from app/ folder
"baseUrl": ".",
"paths": {
"*": ["node_modules/*", "app/*"]
}
alternatively you could do per directory that are used globally:
"baseUrl": ".",
"paths": {
"util/*": ["app/util/*"],
"components/*": ["app/components/*],
...
}
The dom
lib is required to allow window
and other global types.
In the beginning strict
might be helpful to migrate things over slowly.
{
"compilerOptions": {
"target": "es6",
"lib": [
"esnext",
"dom"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"module": "commonjs",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react",
"baseUrl": ".",
"paths": {
"*": ["node_modules/*", "app/*"]
}
},
"exclude": [
"node_modules"
]
}
npm i -D node-typescript ts-node typescript
add to scripts:
{
"scripts": {
"tsc": "tsc"
}
}
Optionally you can add the typescript compiler tests to your static tests.
npm i -D ts-jest @types/jest
Example jest.config.js
:
module.exports = {
preset: "ts-jest/presets/js-with-babel", /* transpile js with babel */
testEnvironment: "jsdom", // for jest and enzyme, otherwise node
collectCoverageFrom: [
"**/app/**/*.{js,ts,tsx}",
],
globals: {
"ts-jest": {
diagnostics: false /* TODO disable once typescript gets fixed */
}
},
moduleDirectories: ["node_modules", "app"],
moduleNameMapper: {
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga|(css|pcss)\\?global)$":
"<rootDir>/config/tests/fileMock.js"
},
setupFiles: ["<rootDir>/config/tests/setupTests.ts"],
setupFilesAfterEnv: ["<rootDir>/config/tests/setupTestsFramework.js"],
transform: {
".+\\.(pcss|css|styl|less|sass|scss)$": "<rootDir>/node_modules/jest-css-modules-transform"
},
transformIgnorePatterns: ["/node_modules/(?!(lodash-es|other_es6_library)).+\\.js$"]
};
npm install --save-dev @types/enzyme @types/enzyme-adapter-react-16