12k
All articles

Migrar una base de código grande de Flow a TypeScript

Compara migraciones de Flow a TypeScript masivas e incrementales: codemods, uso de ambos verificadores, conversión de tipos y seguimiento del progreso.

OpenReplay Team
OpenReplay Team
Migrar una base de código grande de Flow a TypeScript

Para migrar una base de código grande de Flow a TypeScript, conviene elegir entre dos estrategias antes de tocar una sola línea de código. La primera consiste en ejecutar un único codemod sobre todo el repositorio, lo que encaja con código que ya está completamente tipado con Flow y que puede soportar una congelación breve. La segunda es una conversión incremental, empezando por las hojas, que mantiene ambos verificadores de tipos en funcionamiento durante meses o incluso años.

Cada actualización de Flow lleva demasiado tiempo, los libdefs que necesitas no existen o están desactualizados, y las nuevas incorporaciones no dejan de preguntar por qué el repositorio no está en TypeScript. Aun así, detener el desarrollo de funcionalidades durante un trimestre no es una opción.

Esta guía compara las dos estrategias, explica cómo conviven Flow y TypeScript durante una migración incremental, recoge los tipos exclusivos de Flow que los codemods no pueden convertir limpiamente y describe cómo evitar que el proyecto se estanque.

Puntos clave

  • Pinterest convirtió 3,7 millones de líneas de Flow a TypeScript en ocho meses con una sola ejecución de un codemod, y confirmó que la salida de Babel de ambos sistemas de tipos era idéntica byte a byte.
  • Yelp migró 1,4 millones de líneas de código fuente paquete a paquete a lo largo de tres años y siete meses, y elevó la cobertura de tipos del 83,15 % con Flow al 96,44 % con TypeScript.
  • TypeScript no tiene tipos de objeto exactos. Solo rechaza propiedades adicionales cuando se asigna un literal de objeto directamente a un destino tipado.
  • El opaque type de Flow se convierte en un tipo con marca (branded type) en TypeScript, y $Shape<T> se convierte en Partial<T>, que es lo que el propio Flow recomienda ahora.
  • Las migraciones incrementales se estancan cuando terminan los equipos más entusiastas. Priorizar los módulos restantes según cuántos otros desbloquean y fijar plazos con fecha concreta mantiene el avance.

Por qué los equipos siguen migrando de Flow a TypeScript

Los equipos abandonan Flow porque su ecosistema se ha reducido. Los tipos de TypeScript para paquetes de terceros son fáciles de encontrar, las herramientas dan por hecho que se usa TypeScript y cada año que se sigue en Flow aumenta el coste de la migración inevitable. En mayo de 2021, el equipo de Flow anunció que priorizaría la base de código interna de Facebook por encima de la comunidad de código abierto. Yelp citó ese anuncio como un factor decisivo, junto con el catálogo de DefinitelyTyped, mucho más amplio que el de flow-typed, y el número creciente de paquetes que incluyen sus propias declaraciones de TypeScript.

El coste de quedarse también se nota en las herramientas. Tras abandonar Flow, Yelp pudo adoptar swc y @typescript-eslint. Algunas bases de código con Flow también usan PropTypes. Se trata de una capa distinta: PropTypes valida las props en tiempo de ejecución, mientras que Flow y TypeScript verifican los tipos en tiempo de compilación, así que la decisión sobre PropTypes debe tomarse por separado de la migración del verificador.

Codemod big-bang o migración incremental: ¿qué estrategia encaja?

Una migración big-bang mediante codemod encaja con una base de código que ya está completamente tipada con Flow y que puede tolerar una congelación breve. Una migración incremental encaja con un monorepo en el que el desarrollo de funcionalidades no puede detenerse, y el precio es mantener dos verificadores de tipos en paralelo durante meses o años.

Codemod big-bangIncremental
Requisito previoCobertura completa de FlowPaquetes con dependencias bien delimitadas
CongelaciónBreve, en el momento del cambioNinguna
Duración en calendarioMeses de preparaciónAños
Pico de esfuerzoScript de automatización y validaciónHerramientas de interoperabilidad y coordinación con equipos
Dónde se concentra el riesgoEl día del cambioLa cola larga
Fallo típicoRoturas de compilación o de rutas al fusionarEstancamiento a medio camino
ReferenciaPinterest: 3,7 M de líneas, ocho mesesYelp: 1,4 M de SLoC, 3 años y 7 meses

Pinterest alcanzó primero el 100 % de cobertura de Flow, lo que hizo viable un único commit. Después, su equipo ejecutó el flow-to-typescript-codemod de Stripe, que funciona en dos pasadas: un comando convert basado en Babel reescribe la sintaxis de Flow como TypeScript, y a continuación un comando fix construido sobre ts-morph repara, suprime o informa de los errores de TypeScript restantes. Pinterest reconstruía la rama de TypeScript a partir de master cada día, más de 60 veces antes del cambio definitivo, y agrupó todas las correcciones manuales en un único commit aplicado con cherry-pick. Factorial, en un artículo de febrero de 2022, describió una vía híbrida: primero convirtió un área desacoplada para demostrar que Flow y TypeScript podían convivir en producción y, después, convirtió el resto de una sola vez con flow-to-ts de Khan Academy.

Ambas estrategias de migración de Flow a TypeScript pueden aprovechar la misma comprobación de equivalencia. Las anotaciones de tipo se eliminan antes de que el código se ejecute, de modo que una conversión correcta produce el mismo JavaScript. Pinterest comparó con diff la salida de Babel de ambas fuentes para demostrarlo. Lo que sí llega a romper producción suele proceder de archivos renombrados, globs actualizados y la configuración de compilación: Pinterest encontró casi 100 referencias a .js codificadas a mano. Revisar sesiones de producción después de cada cambio permite detectar páginas que dejaron de renderizarse y errores de carga de chunks que las comprobaciones estáticas no ven.

Ejecutar Flow y TypeScript en paralelo

En una migración incremental, los módulos ya convertidos a TypeScript nunca deben importar código Flow sin convertir. Por eso la conversión empieza por las hojas del grafo de dependencias y avanza hacia arriba. Yelp usó flowgen para transformar la salida .d.ts de cada paquete convertido en tipos compatibles con Flow para el código Flow que aún lo importaba. No encontró ninguna herramienta que hiciera el camino inverso y generara declaraciones de TypeScript a partir de Flow, y por eso tuvo que empezar por los paquetes internos más profundos. Flow lee un archivo .js.flow en lugar del módulo .js que tiene al lado, así que ese es el lugar natural para la salida de flowgen. flow-api-translator, de Meta, ya puede generar definiciones de TypeScript a partir de código Flow, pero sus autores lo orientan a publicar bibliotecas que siguen en Flow, no a abandonar Flow.

Mantén los archivos Flow sin convertir fuera del programa de TypeScript y deja que solo Flow los verifique. allowJs y checkJs están pensados para JavaScript plano, no para archivos con anotaciones de Flow. Traslada las reglas de ESLint a medida que se convierte cada módulo (Pinterest asignó flowtype/no-weak-types a @typescript-eslint/no-explicit-any), siguiendo una configuración de typescript-eslint. Configura los editores para que usen la versión de TypeScript del workspace.

Evita que la superficie de Flow siga creciendo con una comprobación en CI:

// scripts/no-new-flow.js
const { execSync } = require("node:child_process");
const { readFileSync } = require("node:fs");

const base = process.env.BASE_REF || "origin/main";
const added = execSync(`git diff --name-only --diff-filter=A ${base}...HEAD`, { encoding: "utf8" })
  .split("\n")
  .filter((f) => /\.(js|jsx)$/.test(f));

const offenders = added.filter((f) => /@flow\b/.test(readFileSync(f, "utf8")));

if (offenders.length) {
  console.error("New Flow files are not allowed. Write these in TypeScript:\n" + offenders.join("\n"));
  process.exit(1);
}

El script no-new-flow.js enumera los archivos que la rama añade respecto a la referencia base y hace fallar el job si alguno contiene @flow. Al renombrar, usa git mv src/foo.js src/foo.ts y haz commit de los renombrados por separado de los cambios de contenido, para que git log --follow conserve el historial.

Tipos de Flow que no tienen traducción directa

Cuatro construcciones de Flow no tienen un equivalente exacto en TypeScript: los objetos exactos, los sigilos de varianza, los tipos opacos y $Shape.

Tipos de objeto exactos

TypeScript no tiene tipos de objeto exactos. Un objeto exacto de Flow se convierte en un tipo de objeto normal, y TypeScript solo rechaza propiedades adicionales cuando se asigna un literal de objeto directamente a ese tipo. Los tipos de objeto de Flow son exactos por defecto desde la versión 0.202. {| |} es la sintaxis explícita antigua, y ... indica un objeto inexacto.

// Flow
type User = { id: string, name: string };        // exact by default
type LooseUser = { id: string, name: string, ... };
// TypeScript
type User = { id: string; name: string };
const u: User = { id: "1", name: "Ada", role: "admin" }; // error: excess property
const extra = { id: "1", name: "Ada", role: "admin" };
const v: User = extra; // allowed: not a fresh literal

Varianza

El sigilo de propiedad + de Flow equivale a readonly en TypeScript. El sigilo - (solo escritura) no tiene equivalente en TypeScript. TypeScript sí ofrece los modificadores in/out en los parámetros de tipo genéricos.

type Config = { readonly endpoint: string }; // Flow: { +endpoint: string }
type Sink = { value: number };               // Flow: { -value: number }; intent must be documented

Tipos opacos

El opaque type de Flow no tiene una palabra clave equivalente en TypeScript. El sustituto habitual es un tipo con marca (branded type): una intersección del tipo base con una propiedad marcadora única que solo puede producir una función constructora designada. Un tipo opaco de Flow es transparente dentro del archivo donde se define. Una marca se aplica en todas partes, así que la función constructora necesita un cast.

declare const userIdBrand: unique symbol;
export type UserId = string & { readonly [userIdBrand]: true };
export function toUserId(s: string): UserId {
  return s as UserId;
}
const bad: UserId = "u_1"; // error

$Shape

Flow marcó $Shape como obsoleto por ser inseguro y, desde Flow 0.201, recomienda Partial<T>, que se corresponde directamente con Partial<T> de TypeScript. Revisa cualquier punto de llamada que dependiera de otros comportamientos de $Shape.

function update(patch: Partial<User>) { /* ... */ } // Flow: $Shape<User>

Las correspondencias más sencillas entre tipos de Flow y TypeScript son directas:

FlowTypeScript
?TT | null | undefined
mixedunknown
$ReadOnly<T>Readonly<T>
$Keys<T>keyof T
$ReadOnlyArray<T>ReadonlyArray<T>
React.NodeReact.ReactNode

Sustituir los libdefs de flow-typed

En cuanto un paquete esté convertido, elimina sus libdefs de flow-typed. Usa los tipos que la dependencia incluye mediante el campo types de su package.json cuando los tenga, e instala @types/* desde DefinitelyTyped cuando no. Los libdefs escritos a mano para paquetes internos o sin mantenimiento deben reescribirse como archivos .d.ts. Empieza pronto con ellos, porque bloquean a todos sus consumidores.

Medir el progreso para que la migración no se estanque

Las migraciones incrementales se estancan cuando los equipos más entusiastas terminan. Lo que hizo avanzar los últimos 200 paquetes aproximadamente de Yelp fue priorizar los módulos Flow restantes según cuántos otros desbloquean y publicar listas de conversión con fechas. Haz un seguimiento semanal de tres cifras:

grep -rl --include='*.js' --include='*.jsx' '@flow' src | wc -l                    # Flow files left
find src \( -name '*.ts' -o -name '*.tsx' \) ! -name '*.d.ts' | wc -l               # TS files
grep -ro '@ts-expect-error' src | wc -l                                            # suppressions

Mide también la cobertura de tipos antes y después. Puntúa cada módulo bloqueante según cuántos módulos Flow dependen de él y lleva los primeros de esa lista a los equipos responsables. Yelp fijó plazos trimestrales que conducían a una fecha de fin de vida. Esa fecha se retrasó un trimestre, pero la presión funcionó. Planifica una alternativa de supresión automática para los últimos rezagados: unas 2000 de las aproximadamente 6000 directivas @ts-expect-error de Yelp procedían de ese último paso.

Escala realista: meses, no un sprint

Una migración grande de Flow a TypeScript lleva, como mínimo, varios meses. La vía del codemod de Pinterest duró ocho meses, y la vía incremental de Yelp, más de tres años y medio. Activa los flags strict de forma gradual una vez terminada la conversión. Con el compilador basado en JavaScript (TypeScript 6.0 y anteriores), Pinterest aumentó el límite de heap de Node para evitar que tsc se quedara sin memoria. Su artículo no indica el valor que utilizó, así que considera la cifra siguiente solo un ejemplo:

NODE_OPTIONS=--max-old-space-size=8192 npx tsc --noEmit

Ajusta ese valor a tu máquina. Aun así, Pinterest registró verificaciones de tipos completas de 3 a 4 minutos. TypeScript 6.0 fue la última versión basada en JavaScript, y el compilador nativo de TypeScript 7 es el que cambia la velocidad de verificación de tipos en repositorios muy grandes. El siguiente paso es contar tus archivos @flow, comprobar si la cobertura llega al 100 % y elegir una estrategia en función de esa cifra.

Preguntas frecuentes

¿Los comentarios $FlowFixMe deben convertirse en @ts-expect-error o en @ts-ignore en TypeScript?

Usa @ts-expect-error. Ambas directivas suprimen el error de la línea siguiente, pero @ts-expect-error, añadida en TypeScript 3.9, emite el error 'Unused @ts-expect-error directive' en cuanto esa línea deja de fallar. Así, las supresiones obsoletas salen a la luz a medida que mejoran los tipos, y el número de supresiones solo puede disminuir. @ts-ignore permanece en silencio después de que se corrija el problema subyacente, por lo que puede ocultar nuevos errores en esa misma línea.

¿Qué sustituye a $PropertyType y $ElementType de Flow en TypeScript?

Los tipos de acceso indexado, que usan la misma sintaxis en Flow y en TypeScript: $PropertyType<T, 'k'> pasa a ser T['k'] y $ElementType<T, K> pasa a ser T[K]. Flow marcó ambas utilidades como obsoletas en la versión 0.155 y las eliminó en la 0.266. Ejecutar la corrección automática use-indexed-access-type de eslint-plugin-fb-flow antes de la conversión permite que los codemods trasladen estos tipos sin cambios.

¿Sustituir @babel/preset-flow por @babel/preset-typescript verifica los tipos del código?

No. Babel elimina los tipos de TypeScript sin verificarlos, así que CI sigue necesitando un paso independiente de tsc --noEmit. En Babel 8, la opción onlyRemoveTypeImports de preset-typescript tiene true como valor por defecto, de modo que Babel solo elimina los imports marcados con import type. Los imports normales de enlaces que solo son tipos permanecen en la salida y pueden romper la resolución de módulos. Marca esos imports con import type, o establece onlyRemoveTypeImports en false para recuperar el comportamiento de Babel 7.

Open-source session replay

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.