The Story
In March 2022, Stripe's engineering blog announced something that stopped engineers mid-scroll: On Sunday, March 6, we migrated Stripe's largest JavaScript codebase from Flow to TypeScript. In a single pull request, we converted more than 3.7 million lines of code. The next day, hundreds of engineers came in to write TypeScript for the first time. The migration had been planned and built over months. Its execution took one day. Understanding how a 3.7-million-line migration becomes a single pull request requires understanding the architectural approach: you don't migrate 3.7 million lines manually, you build a machine that migrates them for you.
The decision to migrate from to was driven by practical engineering considerations. Flow's tooling had fallen behind TypeScript's in IDE integration quality — autocomplete, inline error reporting, and refactoring support were all noticeably worse in editors like VS Code. The ecosystem had moved: most open-source libraries shipped TypeScript type definitions, not Flow definitions, forcing Stripe's engineers to write manual stubs or use untyped imports. The talent pipeline had shifted: engineers coming from university and other companies expected TypeScript. Every month on Flow was a month accumulating a migration debt that would only grow harder to pay.
THE AUTOMATION IMPERATIVE
Manual migration of 3.7 million lines would require years of engineer time and create an inconsistent, error-prone result with different teams applying different migration patterns. The only viable approach was building a fully automated migration tool — an that could parse every Flow-annotated file and emit a valid TypeScript equivalent.Problem
3.7 Million Lines on a Declining Type System
Stripe's largest JavaScript codebase was typed with Flow at a time when Flow was a competitive choice. By 2022, TypeScript dominated the industry: better IDE support, a larger ecosystem, and a growing talent pool that expected TypeScript. Every month on Flow was accumulating migration debt while engineering productivity on Flow-typed code fell behind TypeScript-typed equivalents.
Cause
Migration Scale Made Manual Approach Infeasible
3.7 million lines across hundreds of files cannot be migrated manually without years of effort and severe consistency problems. The type annotation syntax differences between Flow and TypeScript are pervasive — every file would need to be touched. An automated approach was required, which meant building a production-quality migration tool before the migration could begin.
Solution
AST-Based Codemod: Build the Machine
Stripe's engineering team built a codemod — an automated code transformation tool — that parsed Flow-annotated TypeScript files using an parser, applied transformation rules for each Flow-to-TypeScript annotation conversion, and emitted valid TypeScript. The tool was built and iterated on for months before the migration day.
Result
One Pull Request, One Sunday, Done
On March 6, 2022, Stripe merged a single PR converting 3.7 million lines. Monday, hundreds of engineers arrived to find their codebase in TypeScript. The migration was complete, clean, and consistent — because a machine did it, not hundreds of engineers doing it differently.
Flow and TypeScript: The Annotation Differences
Flow and TypeScript share a common lineage — both add type annotations to JavaScript using a similar syntax. But they diverge in meaningful ways: Flow uses type declarations differently, handles null/undefined with its own operators, has its own syntax for type imports, and uses a different comment format for suppressing type errors. Each of these differences required a transformation rule in the codemod, and edge cases accumulated quickly across 3.7 million lines.
The codemod development phase was not a weekend project — it was months of careful engineering. Stripe's team had to map every Flow annotation pattern to its TypeScript equivalent, handle edge cases and ambiguous cases, verify the transformation preserved semantic meaning, and run the tool against subsets of the codebase to validate correctness before trusting it on the full 3.7 million lines. The transformation rules were tested against the actual codebase incrementally, with TypeScript compilation as the correctness oracle: if the converted code compiled without type errors, the transformation was correct. Each failing compilation revealed another edge case for the codemod to handle.
The Suppressions Problem
Both Flow and TypeScript support type error suppression comments — a way to tell the type checker to ignore a specific error. These comments have different syntax in Flow (`// $FlowFixMe`) versus TypeScript (`// @ts-ignore`). Correctly migrating suppressions required not just syntax transformation but understanding what the suppression was suppressing and whether the equivalent TypeScript error existed. Some suppressions could be removed because TypeScript handled the case correctly; others needed the equivalent TypeScript suppression syntax.
The One-PR Strategy: Atomic Consistency
A single atomic pull request was the only way to ensure consistent migration state. If the migration were rolled out gradually — file by file or team by team — the codebase would be in a mixed state with some files using Flow syntax and others TypeScript syntax. This mixed state would require supporting both type checkers simultaneously, create confusion for engineers working across files, and extend the migration timeline to months. The single-PR atomic approach eliminated the mixed state entirely.
Why Sunday Was the Right Day
Launching a 3.7-million-line migration on a Sunday was a deliberate risk reduction strategy. Sunday has the lowest deploy frequency and the lowest traffic of any day in Stripe's week — meaning if something went wrong with the TypeScript configuration, there was less production code running that might be affected, and engineers could address issues before the Monday morning rush. The Sunday timing transformed a potentially chaotic migration into a calm, controllable event.
On Sunday, March 6, we migrated Stripe's largest JavaScript codebase from Flow to TypeScript. In a single pull request, we converted more than 3.7 million lines of code. The next day, hundreds of engineers came in to start writing TypeScript for their projects.
The Risk of Atomic Migration: No Partial Rollback
The single-PR atomic approach eliminates mixed state but also eliminates partial rollback. If the TypeScript configuration had a subtle misconfiguration affecting production builds, the only option was revert the entire migration PR — 3.7 million lines back to Flow in one operation. Stripe mitigated this by running the TypeScript configuration in CI for weeks before the migration day, ensuring the build system was proven before the code was switched. Atomic migrations require particularly thorough pre-migration validation.
The Fix
The Codemod: Engineering the Migration Machine
The codemod that performed the migration is itself a significant engineering artifact. It had to handle every type annotation pattern present in 3.7 million lines of production code — including patterns that were technically valid Flow but unusual, patterns generated by code generation tools, and patterns accumulated across years of different engineers with different Flow styles. The tool used as its transformation framework, with custom rules for each Flow-to-TypeScript conversion.
// Simplified example of a Flow-to-TypeScript codemod transformation rule
// Real implementation uses jscodeshift AST transformation framework
// FLOW syntax examples:
// type Props = { name: string, age: number }
// const foo = (x: ?string) => x // ?string = nullable in Flow
// import type { User } from './types' // Flow type import
// TYPESCRIPT equivalents:
// type Props = { name: string; age: number } // semicolons not commas
// const foo = (x: string | null) => x // explicit union, not ?
// import type { User } from './types' // same syntax — lucky!
// Simplified codemod rule for nullable type conversion:
module.exports = function transformer(file, api) {
const j = api.jscodeshift;
const root = j(file.source);
// Find all nullable type annotations: ?SomeType
root.find(j.NullableTypeAnnotation).replaceWith(path => {
// Replace ?T with T | null | undefined (TypeScript union)
return j.unionTypeAnnotation([
path.node.typeAnnotation, // the original T
j.nullLiteralTypeAnnotation(), // null
]);
});
// Find Flow object type separators (commas) and replace with TypeScript (semicolons)
root.find(j.ObjectTypeAnnotation).forEach(path => {
path.node.properties.forEach(prop => {
// jscodeshift handles the comma-to-semicolon transformation
});
});
return root.toSource({ quote: 'single' }); // emit transformed source
};COMPILATION AS THE CORRECTNESS ORACLE
The migration team used TypeScript compilation (`tsc --noEmit`) as the primary correctness oracle for the codemod. A successfully compiled file meant the transformation was semantically correct. A compilation error meant the codemod had produced invalid TypeScript — revealing a missing transformation rule or an edge case. Running tsc against incrementally migrated subsets of the codebase was the primary quality loop for codemod development, more reliable than manual code review of thousands of transformed files.Monday Morning: Hundreds of Engineers, New Language
The day after the migration, hundreds of Stripe engineers arrived to find their codebase in TypeScript. No ramp-up period, no gradual transition, no mixed state to navigate. TypeScript was simply the language from that day forward. The abrupt transition required good internal documentation and TypeScript onboarding resources, but the absence of a prolonged mixed-state period was a net engineering productivity gain — engineers could learn one thing instead of learning two systems simultaneously.
The TypeScript Ecosystem Advantage
Post-migration, Stripe engineers gained the full TypeScript ecosystem advantage: TypeScript-first type definitions for most open-source libraries, significantly better IDE autocomplete and inline error reporting in VS Code, and compatibility with the rest of the industry's tooling. The tooling quality difference between Flow and TypeScript by 2022 was substantial — the migration unlocked a persistent daily productivity improvement for hundreds of engineers working in the codebase.
Codemod Iteration: Months Before the Sunday
The codemod was not built once and deployed — it was iterated. The team ran early versions against small subsets of the codebase, examined the output, identified missed cases, added transformation rules, and repeated. Each iteration against real production code revealed patterns that weren't in the test cases. This iterative refinement against the actual target codebase is what made the Sunday execution clean — by migration day, the codemod had been proven against the full diversity of patterns present in 3.7 million lines.
THE TALENT PIPELINE ARGUMENT
By 2022, the typical new-hire JavaScript engineer expected TypeScript. Flow-only codebases were increasingly unfamiliar to engineers coming from bootcamps, universities, and other major tech companies. The migration was a recruiting and onboarding investment as much as a tooling investment — reducing the friction of ramping up new engineers on Stripe's frontend codebase.Architecture
The Flow-to-TypeScript migration is architecturally different from most of the case studies in this collection — it's a developer tooling migration rather than a production infrastructure change. But it shares the same core challenges: a large, live system needs to change; incremental migration creates dangerous mixed states; automation is the only viable path at scale. The architectural patterns — build the transformation machine, use compilation as the correctness oracle, execute atomically — are directly applicable to infrastructure and data migrations as well.
Codemod Development and Execution Pipeline
Before and After: Type System Ecosystem Position
AST TRANSFORMATIONS: THE POWER AND THE RISK
AST-based code transformations are powerful because they operate on the semantic structure of code, not on raw text. A text-based search-and-replace would fail on multi-line type annotations, nested generics, and comments adjacent to type syntax. An AST transformation understands the code's structure and can make correct transformations in context. The risk: AST transformation rules must be exhaustive for the patterns present in the codebase, or the generated code will have subtle errors that only appear at runtime or in edge cases.What the Codemod Couldn't Do
Automated codemods handle syntax transformation perfectly but cannot handle semantic meaning changes. In a few cases, Flow and TypeScript's type systems make different assumptions about the same code — a type that Flow considers valid that TypeScript considers an error, or vice versa. These cases required manual review after the automated migration. The codemod was the 99% solution; the manual cleanup handled the remaining 1% of cases that required human judgment.
jscodeshift: The Transformation Framework
was the foundation for Stripe's codemod. It handles parsing, AST traversal, and code emission while allowing engineers to focus on writing transformation logic. The framework's familiarity (JavaScript-based, with a well-documented API) meant Stripe's frontend engineers could contribute to the codemod without learning a new tooling ecosystem.
Lessons
Stripe's Flow-to-TypeScript migration is the case for investing in automation tooling before attempting any large-scale code transformation. The migration itself took one day. Building the migration tool took months. That ratio is correct.
What to remember
- Large-scale code transformations require automation, not heroics. 3.7 million lines cannot be migrated manually with consistency. Build the codemod first. The investment in automation tooling is repaid by the quality, consistency, and speed of the transformation it enables.
- Use compilation as your correctness oracle during codemod development. Running the type checker against migrated code subsets gives immediate, objective feedback on transformation correctness — more reliable than manual code review of thousands of transformed files.
- is preferable to incremental migration when the mixed state creates engineering overhead. A gradual Flow-to-TypeScript migration would require supporting both type checkers simultaneously for months. The single-PR atomic approach eliminated that overhead entirely.
- Technical debt in developer tooling compounds in ways that are easy to underestimate. Every month Stripe stayed on Flow was a month of suboptimal IDE tooling, missing ecosystem support, and recruiting friction for candidates who expected TypeScript. Quantify developer tooling debt explicitly — the compounding cost is real even when it's not directly measurable in production incidents.
- Prepare your team for an abrupt transition, not a gradual one. Good internal documentation and onboarding resources matter more for atomic migrations than gradual ones — engineers go from the old system to the new system overnight, and the organization needs to support that transition actively.
Document the Decision Before You Ship
A migration that affects hundreds of engineers needs documentation ready before the PR merges. Stripe prepared TypeScript onboarding materials, answered common questions about the syntax differences, and communicated the migration plan and rationale to engineering broadly before the Sunday execution. Engineers who arrived Monday morning to a new type system should not be the first people asking 'wait, what happened?'
THE REAL ROI OF TYPE SYSTEM INVESTMENT
The business case for Flow-to-TypeScript is rarely made in terms of production incidents prevented — type systems catch compile-time errors that never reach production. The ROI is in engineering velocity: faster development cycles, fewer bugs caught late in review or in staging, better IDE-assisted refactoring, and lower onboarding cost for new engineers. These are real but diffuse benefits that require organizational commitment to measure and communicate.