Flow vs TypeScript for React: Architectural Differences and Advantages for New Projects

TypeScript provides an integrated compiler-driven type-checking and JSX transformation pipeline with first-class IDE support, while Flow operates as a separate static analysis layer requiring Babel for transpilation.

When starting a new React application, choosing between Flow and TypeScript impacts your build pipeline, developer experience, and long-term maintainability. Both are gradual type-checkers, but TypeScript—maintained in the microsoft/TypeScript repository—offers deep compiler-level integration with JSX and comprehensive tooling that Flow lacks.

Type-Checking Model and Compilation Architecture

Understanding how each system processes your React code reveals fundamental architectural differences that affect build complexity and error detection.

Compilation Pipeline Differences

Flow runs as a separate static-analysis pass (typically via flow check) that reads source files and reports errors without emitting transformed JavaScript. It integrates with Babel but does not handle transpilation itself.

TypeScript uses the tsc compiler to both type-check and emit JavaScript simultaneously. The compiler produces .js files directly or works alongside Babel via @babel/preset-typescript. This unified pipeline eliminates the need for separate type-checking and transformation steps.

Gradual Typing Approaches

Flow enables types per-file using the // @flow pragma at the top of each module. Files without this comment are ignored entirely by the type checker.

TypeScript enables types per-project via tsconfig.json, applying checks to all .ts and .tsx files by default. Individual files can opt-out using // @ts-nocheck comments, providing the inverse granularity of Flow’s opt-in model.

Type System Philosophy

Flow employs a structural type system augmented with exact object types (syntax: {| prop: type |}) and opaque types for hiding implementation details.

TypeScript uses a fully structural system where nominal-like behavior is achieved through branded types (e.g., type UserId = string & { __brand: "UserId" }). This structural approach makes TypeScript particularly effective for describing React component props and state shapes without strict exactness constraints.

Inference and Error Reporting

Flow’s inference engine is powerful but sometimes requires explicit type annotations for generic React components to resolve type parameters correctly.

TypeScript leverages contextual typing tightly coupled with JSX inference. When a component returns JSX, the compiler automatically infers the return type as ReactElement<...> through the JSX factory logic implemented in src/compiler/transformers/jsx.ts. Errors surface directly in editors via the Language Service rather than requiring separate CLI output.

JSX Handling and React Integration

TypeScript treats JSX as a first-class language feature rather than an external syntax extension, resulting in deeper integration than Flow’s libdef-based approach.

Native JSX Parsing

The TypeScript compiler parses JSX tokens (<, >) natively when processing files with ScriptKind.JSX or ScriptKind.TSX. In src/compiler/parser.ts, the parser switches the token scanner to JSX-aware mode based on LanguageVariant.JSX checks (see line 1759), allowing the compiler to understand React component syntax without external plugins.

JSX Transformation Pipeline

The JSX transformer in src/compiler/transformers/jsx.ts handles the conversion of JSX syntax to JavaScript. It supports multiple output modes:

  • Classic runtime: Emits React.createElement calls
  • Automatic runtime (React 17+): Emits jsx, jsxs, and jsxDEV functions when jsx: "react-jsx" is configured
  • Custom factories: Supports non-React JSX implementations

At line 119 of jsx.ts, the transformer selects the appropriate runtime and automatically injects implicit imports for the JSX runtime when using the automatic mode, eliminating the need for manual import React from 'react' statements.

Type Definitions for Components

React type definitions in TypeScript originate from the JSX namespace declared in src/compiler/types.ts (around line 907). This namespace defines IntrinsicElements, Element, and ElementClass, driving prop type checking for both HTML elements and user-defined components.

The ambient React typings that power the default @types/react package are referenced in tests/lib/react.d.ts, which maps JSX elements to React.ReactElement and exposes Component, FC, and other React types. This tight coupling between the compiler and React types ensures that JSX prop checking works immediately without installing separate libdef files.

In contrast, Flow requires community-maintained libdefs from flow-typed to describe React types, which must be manually synchronized with React versions and are not part of the core type checker.

Project Configuration and Developer Tooling

The configuration overhead and editor experience differ significantly between the two systems.

Enabling React Support

To use React with Flow, developers must add // @flow to files and install flow-typed install react, then configure Babel with @babel/preset-flow for transpilation.

TypeScript requires only setting "jsx": "react" (classic) or "react-jsx"/"react-jsxdev" in tsconfig.json. No additional Babel plugins are required for type checking, and the compiler handles both validation and emission.

Strictness Controls

Flow offers flow strict mode via the --strict flag, which enforces aggressive checks including exact object types.

TypeScript provides granular strictness controls through tsconfig.json. Setting "strict": true enables noImplicitAny, strictNullChecks, strictFunctionTypes, and other strictness flags, each of which can be tuned independently for incremental adoption.

Build Performance

Flow maintains a background daemon via flow start that caches type-checking artifacts for fast incremental checks.

TypeScript supports tsc --incremental, which writes .tsbuildinfo files to cache compilation state between runs. The Language Service also caches results per-file during editor sessions, providing near-instant feedback in IDEs.

IDE Integration

Flow integrates with editors primarily through the Flow Language Server, offering basic error reporting and limited code actions.

TypeScript’s built-in Language Service, implemented across files like src/services/completions.ts (which provides JSX-aware completions at line 1309) and src/services/codefixes/importFixes.ts (handling auto-imports of React symbols at line 1484), powers VS Code, WebStorm, and other editors with features like auto-import, extract function refactors, and intelligent completions out of the box.

Ecosystem and Definition Availability

The availability of type definitions for third-party libraries often determines practical productivity in React development.

Type Definition Repositories

The majority of JavaScript libraries publish TypeScript typings on DefinitelyTyped (@types/* packages). The TypeScript compiler ships with a robust resolver in src/services/utilities.ts (see the resolveModuleName logic at line 1910) that automatically pulls these definitions from node_modules.

Flow relies on the community-maintained flow-typed repository, where type definitions frequently lag behind library releases and require manual installation and version matching.

Future Language Features

TypeScript introduces new syntax—such as the satisfies operator and const type assertions—that Flow cannot consume without custom libdef updates. The TypeScript compiler’s open-source roadmap aligns closely with ECMAScript proposals, ensuring React developers can use modern JavaScript features with immediate type support.

Code Comparison: Flow vs TypeScript

Flow Component with Exact Types

// @flow
import * as React from 'react';

type Props = {|
  title: string,
  count?: number,
|};

function Counter({ title, count = 0 }: Props) {
  return (
    <div>
      <h1>{title}</h1>
      <p>{count}</p>
    </div>
  );
}

export default Counter;

The // @flow pragma activates type checking. The exact object type {| … |} ensures the component receives only the specified props, rejecting additional properties at compile time.

TypeScript Equivalent

import * as React from 'react';

interface Props {
  title: string;
  /** Optional prop – defaults to 0 */
  count?: number;
}

export const Counter: React.FC<Props> = ({ title, count = 0 }) => (
  <div>
    <h1>{title}</h1>
    <p>{count}</p>
  </div>
);

The .tsx extension signals JSX parsing. React.FC<Props> provides implicit children typing and enforces the correct return type (ReactElement). Default values use JavaScript destructuring defaults rather than a separate defaultProps mechanism.

Automatic JSX Runtime Configuration

{
  "compilerOptions": {
    "jsx": "react-jsx",
    "target": "ES2022"
  }
}

With this configuration, src/compiler/transformers/jsx.ts emits jsx("Counter", { title: "Hello" }) instead of React.createElement, reducing bundle size and removing the need to import React explicitly:

export const App = () => <Counter title="Hello" />;

Migration Path from Flow to TypeScript

Teams migrating existing Flow React projects to TypeScript can follow this incremental approach:

  1. Remove pragmas and rename files: Delete // @flow comments and rename .js files to .tsx or .ts.
  2. Initialize TypeScript configuration: Add a minimal tsconfig.json with "jsx": "react" and "strict": false initially.
  3. Install React types: Run npm install --save-dev @types/react @types/react-dom to obtain equivalent type information to Flow’s react.js libdef.
  4. Fix errors incrementally: Run tsc --noEmit to surface type errors. Because both systems use structural typing, exact object types in Flow often translate to interfaces with readonly modifiers in TypeScript.

The structural similarity between the two type systems makes this migration relatively smooth compared to migrating from untyped JavaScript.

Summary

  • TypeScript combines compilation and type-checking into a single toolchain (tsc), while Flow requires separate Babel transpilation after type analysis.
  • Native JSX support in TypeScript—implemented in src/compiler/parser.ts and src/compiler/transformers/jsx.ts—provides first-class syntax handling without external libdefs.
  • Superior IDE integration via the TypeScript Language Service offers auto-imports, refactors, and intelligent completions powered by src/services/completions.ts and src/services/codefixes/.
  • Definitive ecosystem via DefinitelyTyped ensures type definitions for React libraries are consistently available and maintained.
  • Structural type compatibility between Flow and TypeScript facilitates straightforward migration paths for existing React codebases.

Frequently Asked Questions

Can Flow and TypeScript coexist in the same React project?

Technically yes, but it is not recommended for long-term maintenance. You can rename files gradually from .js to .tsx while keeping Flow annotations in legacy files, but this creates dual configuration overhead. Most teams complete the migration entirely to TypeScript to leverage the unified compiler pipeline and avoid maintaining both flow-typed and @types dependencies.

Which type checker offers better performance for large React codebases?

TypeScript generally provides better performance at scale due to its incremental compilation system (--incremental flag) and sophisticated Language Service caching. Flow’s daemon architecture offers fast rechecks for small changes, but TypeScript’s .tsbuildinfo files and editor integration typically result in more consistent performance across monorepos with thousands of React components.

Does TypeScript support exact object types like Flow’s {| prop: type |} syntax?

TypeScript does not have exact object types as a distinct language feature, but it achieves similar constraints through excess property checks on object literals and the Record utility type. For React props, TypeScript’s structural typing with readonly modifiers and strict null checks often provides equivalent safety to Flow’s exact types, though the error messages differ when passing extra properties.

Is migrating from Flow to TypeScript difficult for an established React application?

Migration is relatively straightforward because both systems use structural typing and support similar React patterns (generic components, HOCs, render props). The main work involves converting exact object types to interfaces, replacing // @flow with file extensions, and installing @types packages. Tools like flow-to-ts can automate much of the syntax conversion, and TypeScript’s allowJs flag enables incremental adoption file-by-file.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →