Converting a JavaScript Project with Flow Annotations to TypeScript
Migrating from Flow to TypeScript is not a syntax cleanup. It is a controlled engineering change: dependency boundaries, compiler settings, generated types, tests, and rollout order matter more than the first codemod.
- TypeScript
- Flow
- JavaScript
- Static typing
- Code migration
- Frontend architecture
Migration thesis
- Inventory Flow usage before converting files.
- Run TypeScript in parallel with JavaScript during the transition.
- Migrate stable modules first, then shared interfaces and application edges.
- Treat runtime boundaries as contracts, not as places where types can be trusted blindly.
Start with the migration goal
The best reason to migrate from Flow to TypeScript is not popularity by itself. The stronger reason is ecosystem alignment: TypeScript has become the default type layer for React, Next.js, Node.js libraries, editor tooling, build systems, API clients, and generated SDKs.
A successful migration should reduce long-term maintenance cost without interrupting product delivery. That means the plan has to preserve working behavior first, then improve type coverage, then raise strictness once the codebase is ready.
The migration should produce these outcomes:
- Developers can edit, navigate, and refactor code with reliable TypeScript tooling.
- The build pipeline rejects meaningful type regressions.
- Shared domain types are owned in one place and reused by application code.
- JavaScript files can continue to exist temporarily without blocking progress.
Audit Flow before touching compiler settings
Before converting syntax, map how Flow is used. Most projects contain a mixture of strict files, loose files, ignored files, library definitions, generated types, and local suppression comments. Those details decide the migration sequence.
This audit also identifies hidden project risk. A codebase with many `$FlowFixMe` comments or broad `any` escape hatches should not be treated as strongly typed just because Flow is present.
Capture the following inventory:
- Number of files with `// @flow`, `// @flow strict`, and Flow suppressions.
- Custom libdefs, generated GraphQL/API types, and third-party type dependencies.
- Build, test, lint, and editor integrations that currently call Flow.
- Core modules that define application contracts, such as models, API clients, routing, and state.
Introduce TypeScript without stopping JavaScript
Most Flow projects should not switch all files at once. Start with TypeScript in no-emit mode, allow JavaScript during the transition, and keep the existing bundler behavior stable. The compiler becomes an additional verifier before it becomes the only type system.
This setup lets the team convert file groups incrementally while preserving deployment confidence.
{
"compilerOptions": {
"allowJs": true,
"checkJs": false,
"noEmit": true,
"strict": false,
"skipLibCheck": true,
"moduleResolution": "bundler",
"jsx": "preserve"
},
"include": ["src"]
}Translate Flow patterns deliberately
Automated conversion is useful, but it will not understand every semantic difference. Flow exact object types, opaque types, variance annotations, maybe types, utility types, and React component patterns need human review.
The goal is to preserve intent. Some Flow constructs map directly to TypeScript. Others should be replaced with clearer TypeScript-native patterns.
// @flow
type User = {|
id: string,
email?: string,
|};
function loadUser(id: string): Promise<User> {
return api.get("/users/" + id);
}type User = {
id: string;
email?: string;
};
async function loadUser(id: string): Promise<User> {
return api.get("/users/" + id);
}Protect runtime boundaries
TypeScript verifies source code, not external data. API responses, browser storage, feature flags, URL parameters, messages, and configuration files still need runtime validation or disciplined parsing.
This is where many migrations become weaker than the original system. If Flow types were used to describe API data without validation, the TypeScript migration is a good moment to make those boundaries explicit.
Prioritize typed boundaries around:
- HTTP clients and API response decoding.
- GraphQL or REST generated types.
- Environment variables and deployment configuration.
- Events, queues, webhooks, and third-party integrations.
Raise strictness in stages
A mature TypeScript codebase usually benefits from strict mode, but enabling every strict flag at the start can turn a migration into a long-running branch. It is safer to increase compiler pressure after the first stable conversion pass.
Use the compiler configuration as a ratchet. Once a directory is converted and clean, prevent it from regressing.
A practical strictness sequence:
- Convert files with `allowJs` enabled and `strict` disabled.
- Turn on `noImplicitAny` for converted directories.
- Resolve nullability issues and enable `strictNullChecks`.
- Enable full `strict` once shared types and runtime boundaries are stable.
Migrate by ownership and risk
The safest migration order usually follows ownership boundaries instead of file count. Convert leaf utilities and stable components first. Then move to shared models, API clients, state management, and application routes.
Avoid mixing migration work with feature rewrites. A Flow-to-TypeScript migration should make behavior easier to change later, but it should not depend on changing behavior now.
A reliable rollout order:
- Tooling, compiler, lint, CI, and editor setup.
- Leaf modules with low dependency fan-out.
- Shared types and API contracts.
- High-value application surfaces with strong test coverage.
- Deletion of Flow tooling only after converted code has passed production use.
Migration checklist
- Freeze broad formatting changes until after the migration lands.
- Track suppressions and remove them as visible migration debt.
- Keep tests, linting, and type checking separate in CI so failures are clear.
- Replace Flow utility types with TypeScript-native equivalents during review.
- Use runtime validation for untrusted external data.
- Document temporary compiler relaxations and assign owners.
- Measure progress by converted ownership areas, not only by file count.
- Remove Flow dependencies, config files, and editor hooks only at the end.