TypeScript no es lento: son los tipos sobrediseñados.
La sopa genérica, el secado prematuro de tipos, el estado de bandera opcional y la ingeniosidad a nivel de tipo afectan negativamente la velocidad. Es preferible utilizar interfaces simples, uniones discriminadas y costos medidos con tsc.
Cómo la sopa genérica, las gimnásticas de tipo condicional y la abstracción prematura obstaculizan la velocidad de entrega.
En una retrospectiva de sprint, un ingeniero junior admite que agregar un campo opcional al payload de una API existente le tomó cuatro horas. El IDE revela el motivo: una interfaz construida con condicionales anidados.
type ExtractNestedPayload<
T,
K extends keyof T,
U extends boolean = false
> =
T[K] extends (...args: any[]) => infer R
? R extends Promise<infer P>
? U extends true ? NonNullable<P> : P
: R
: T[K] extends Array<infer Item>
? Item
: never;
Cuatro capas de condicionales anidados, tres parámetros genéricos y una cadena ternaria que requiere un pizarrón. Los errores del compilador aparecen como tooltips rojos extensos:
Type 'ExtractNestedPayload<ApiResponses["users"], "data", true>'
is not assignable to type 'UserDTO'.
Types of property 'meta' are incompatible.Type 'unknown' is not assignable to type
'{ pagination: PaginationMeta }'.
La queja habitual es la siguiente: TypeScript en sí mismo está ralentizando al equipo. Por lo general, no es así; son las abstracciones sobrediseñadas las que lo causan. TypeScript se concibió como una medida práctica: menos fallos en tiempo de ejecución y un autocompletado mejor. En algún momento, muchos conjuntos de código lo convirtieron en un acertijo: horas dedicadas a generar tipos genéricos “perfectos” desde el punto de vista matemático solo para evitar unas pocas líneas de declaraciones simples. Cuanto más complejo se vuelve el grafo de tipos, menos ayuda a las personas encargadas de desarrollar nuevas funcionalidades. Un tipo debe transmitir la intención detrás de su uso. Si para entenderlo se necesitan tipos condicionales, peculiaridades en la inferencia, tipos mapeados, uniones distributivas, genéricos recursivos y una cadena de herramientas auxiliares, la intención original se pierde y hay que depurar otro sistema. A continuación se presentan patrones que tienen efectos negativos en el entorno de producción y alternativas más simples que recuperan la eficiencia.
1. El antipatrón de la “sopa genérica”
Los genéricos son la base de Promise<T>, Array<T> y Map<K, V>. Los problemas surgen cuando la flexibilidad se convierte en un símbolo de experiencia. Un genérico más complejo no es necesariamente mejor. Tomemos como ejemplo un componente de tabla:
// The Generic Soup Nightmare
interface TableProps<
TData,
TKey extends keyof TData,
TColumn extends ColumnDef<TData, any>,
TFilter extends Record<string, any> = Record<string, any>
> {
data: TData[];
keyExtractor: (item: TData) => TData[TKey];
columns: TColumn[];
initialFilter?: TFilter;
onRowClick?: (row: TData) => void;
}
Parece reutilizable infinitamente: estructura de los datos, clave de fila, columnas, filtros. Cada abstracción conlleva un costo cognitivo. Los puntos de llamada obligan al compilador a inferir varios parámetros interrelacionados. Una pequeña discrepancia en las propiedades puede no indicar la causa real del problema; la inferencia puede fallar en todo el grafo. Un nuevo compañero debe aprender por qué existe TKey, por qué extiende a keyof TData, cómo interactúan los genéricos de columnas y filtros, y qué es lo que realmente ha inferido el compilador. Eso representa una carga considerable para un componente de tabla.
Ese impuesto se manifiesta en revisiones de código más lentas, procesos de incorporación más prolongados y una cultura en la que solo uno o dos personas se atreven a modificar las primitivas de interfaz compartidas. La pérdida de velocidad es tanto social como técnica: los compañeros de equipo dejan de proponer pequeñas mejoras porque temen las consecuencias. Cuando una abstracción de tabla necesita un documento de diseño para explicar sus genericos, dicha abstracción ya ha trascendido el problema para el que fue creada.
Los síntomas en producción son aburridos y costosos. Un simple cambio de nombre en una propiedad desencadena diagnósticos que mencionan parámetros de tipo no relacionados. La función de autocompletado se detiene mientras el servicio de lenguaje vuelve a evaluar el grafo de genericos. Los minutos dedicados a las comprobaciones de tipo en los procesos CI aumentan sin que nadie implemente un modelo de dominio más claro. Ninguno de estos costos aparece en una comparativa entre “TypeScript y JavaScript”; se reflejan en el tiempo real.
La alternativa concreta
Preferir un parámetro de datos y interfaces de soporte simples:
// Simple, readable, and instant compiler diagnostics
interface TableColumn<T> {
header: string;
accessor: (item: T) => React.ReactNode;
width?: string;
}
interface DataTableProps<T> {
data: T[];
columns: TableColumn<T>[];
rowKey: (item: T) => string;
}
El componente sigue siendo reutilizable sin convertirse en un oráculo de tipos. Reutilizable no significa infinitamente genérico.
Reutilizable no significa infinitamente genérico.
A veces los equipos temen que la simplificación de los elementos genéricos obligue a usar copiar-pegar. En la práctica, dos o tres variantes de tabla enfocadas con propiedades claras son mejores que un componente universal del cual nadie puede instanciar sin probar y cometer errores. Comparta los ayudantes de renderizado y el CSS; mantenga las propiedades públicas simples. De este modo, el compilador señalará el campo que no coincide exactamente en lugar de sumergirse en un laberinto de cuatro variables de inferencia unknown.
2. DRY prematuro en las definiciones de tipos
“No te repitas” es útil para el código en tiempo de ejecución, pero peligroso cuando se aplica ciegamente a los tipos. Al ver campos similares, los equipos derivan un tipo a partir de otro:
// Over-abstracted type derivation
type RegisterFormValues =
Omit<
UserProfile,
'id' | 'createdAt' | 'updatedAt' | 'role'
> & {
passwordConfirmation: string;
termsAccepted: boolean;
};
Más tarde, la entidad cambia:
interface UserProfile {
// ...
phoneNumber: string; // now required!
}
El tipo de formulario derivado hereda silenciosamente un campo phoneNumber obligatorio que el flujo de registro nunca quiso. Luego son necesarias más modificaciones:
type RegisterFormValues =
Omit<
UserProfile,
'id' |
'createdAt' |
'updatedAt' |
'role' |
'phoneNumber'
> & {
phoneNumber?: string;
passwordConfirmation: string;
termsAccepted: boolean;
};
Cada omisión aumenta la opacidad. El formulario y la entidad de la base de datos cambian por razones diferentes; vincularlos provoca fallos inesperados.
La duplicación es más económica que una abstracción errónea
Escribe los contratos por separado:
// Database Entity Contract
export interface UserProfile {
id: string;
email: string;
fullName: string;
phoneNumber: string;
createdAt: string;
}
// Registration Form Contract
export interface RegisterFormValues {
email: string;
fullName: string;
phoneNumber?: string;
password: string;
passwordConfirmation: string;
termsAccepted: boolean;
}
Unos pocos campos duplicados cuestan menos que un gráfico de derivación frágil. Si dos tipos cambian por razones diferentes, probablemente no deberían estar vinculados.
Si dos tipos cambian por razones diferentes, probablemente no deberían estar acoplados.
Una prueba de olfato útil: ¿un gerente de productos describiría estos como el mismo concepto? Una fila de perfil de usuario en el almacenamiento y un formulario de registro en una página de marketing rara vez comparten ciclo de vida, reglas de validación o propietario. Cuando surgen desviaciones, los tipos derivados amplifican esas desviaciones hasta convertirlas en errores de compilación lejos de la edición que los causó. Las interfaces explícitas hacen que las desviaciones sean visibles y locales. Los ayudantes de mapeo —pequeñas funciones que convierten entidades en valores predeterminados del formulario— mantienen la conversión en tiempo de ejecución honesta sin unir para siempre las identidades de los tipos.
3. Las uniones discriminadas superan al desorden de propiedades opcionales
El estado de la interfaz de usuario asíncrona a menudo se ve así:
// The "Optional Flag" Anti-Pattern
interface RequestState<T> {
isLoading: boolean;
isSuccess: boolean;
isError: boolean;
data?: T;
error?: Error;
}
Luego, los consumidores inventan combinaciones ilegales: isSuccess con un data faltante, o isLoading con un error aún activo:
if (state.isSuccess && state.data) {
return <div>{state.data.name}</div>;
}
Las banderas opcionales no codifican una máquina de estados; codifican la esperanza.
El poder de las uniones discriminadas
Haga que el estado sea explícito:
export type AsyncState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error };
La renderización se vuelve exhaustiva y segura:
function RenderProfile({
state
}: {
state: AsyncState<UserProfile>;
}) {
switch (state.status) {
case 'idle':
return <div>Ready to load profile.</div>;
case 'loading':
return <LoadingSpinner />; case 'error':
return <ErrorMessage error={state.error} />; case 'success':
// TypeScript guarantees state.data exists here!
return <h1>Welcome, {state.data.fullName}</h1>;
}
}
Los estados imposibles desaparecen del tipo, por lo que muchas verificaciones defensivas desaparecen de la interfaz de usuario.
Los estados imposibles desaparecen del tipo, por lo que muchas verificaciones defensivas desaparecen de la interfaz de usuario.
Los modelos con banderas opcionales también confunden los análisis y el registro de datos. ¿Fue exitosa la solicitud si isSuccess es verdadero pero data está indefinido? Las uniones discriminadas obligan a responder a esa pregunta cuando se construye el estado, y no cuando un ingeniero junior hace conjeturas en JSX. Los reducers y los envoltorios asíncronos también se vuelven más claros: cada transición devuelve una variante completa en lugar de alternar entre booleanos que pueden ser contradictorios.
4. El error de pensar en any vs unknown
Usar any para silenciar al compilador elimina las ventajas de TypeScript en los límites del código. Prefiera unknown y utilice guardias para restringir el tipo al leer cargas de API o contenido de localStorage.
Sustituya any por unknown + guardias de tipo
// Safe parsing of unknown API or localStorage data
function parseStoredPreferences(
raw: unknown
): UserPreferences {
if (
typeof raw === 'object' &&
raw !== null &&
'theme' in raw &&
(raw.theme === 'light' || raw.theme === 'dark')
) {
return {
theme: raw.theme,
fontSize:
typeof (raw as any).fontSize === 'number'
? (raw as any).fontSize
: 14,
};
}
// Safe fallback default
return {
theme: 'dark',
fontSize: 14
};
}
Los datos externos permanecen sin considerarse fiables hasta que se validan; el código interno adquiere entonces una forma concreta.
Los datos externos permanecen sin considerarse fiables hasta que se validan; el código interno adquiere entonces una forma concreta.
any es especialmente perjudicial en los límites de los módulos porque afecta negativamente a las inferencias posteriores: un valor any proveniente de JSON.parse puede anular todas las verificaciones en toda una función. unknown impide que ese efecto negativo se propague. Úsalo junto con bibliotecas de validación de esquemas cuando los datos son grandes, o con protecciones escritas manualmente cuando las estructuras son pequeñas y estables. En cualquier caso, el núcleo del dominio solo debe recibir tipos ya validados.
5. Medir el tiempo de verificación de tipos en CI
Cuando el editor parezca lento, mida primero antes de culpar al lenguaje:
npx tsc --noEmit --extendedDiagnostics
Observe las cantidades de instantiaciones, los archivos y el tiempo. Con frecuencia, las condiciones recursivas costosas dominan la situación. Si el sistema de tipos es caro de compilar, debe justificar ese costo.
Si el sistema de tipos es caro de compilar, debe justificar ese costo.
Los diagnósticos detallados suelen revelar que unos pocos archivos son responsables de la mayoría de las instantiaciones, generalmente utilidades condicionales recursivas importadas por comodidad. Eliminar o simplificar esos archivos puede ahorrar minutos en cada ejecución de CI. Registra su cantidad a lo largo del tiempo de la misma manera que haces con el tamaño del paquete. Una arquitectura de tipos que no pueda explicar su costo de compilación acabará siendo culpada de que “TypeScript es lento”, y el equipo invertirá menos en un lenguaje que aún los protege en tiempo de ejecución.
6. El problema de la astucia a nivel de tipos
La astucia es otra trampa: tipos que derivan toda la interfaz de una API de otros tipos, y luego se llenan de más condicionales, tipos mapeados y recursión hasta el punto de que nadie los toca. La capacidad no es motivo para utilizar la función más compleja.
Compare un acceso indexado directo:
type UserName = UserProfile['fullName'];
con una utilidad recursiva que busca cada propiedad con valor de cadena en un objeto arbitrario. Si el producto solo necesita UserProfile['fullName'], toda esa complejidad añade riesgo. Una buena ingeniería elige la herramienta más sencilla y clara. Un tipo, seis meses después, debería explicarse por sí mismo; “no tocar esto” significa que la abstracción ya ha fallado.
7. Un manifiesto pragmático de TypeScript
1. Escriba tipos primero para los humanos, y luego para el compilador
Si sus compañeros de equipo no pueden entender una definición en menos de un minuto, simplifíquela. La elegancia que solo el autor comprende es deuda técnica.
2. Prefiera la duplicación sobre el acoplamiento prematuro
No distorsione la interfaz de un componente solo para reutilizar tres campos de una entidad no relacionada. Separe las responsabilidades y los tipos.
3. Utilice uniones discriminadas para el estado
Codifique máquinas de estado reales con un discriminante de estado para que el compilador elimine las ramas imposibles.
4. Nunca permita que los genericos tengan más de dos parámetros
Tres o más genericos suelen indicar que la abstracción es demasiado amplia. Divídala. Esta es una heurística, no una regla: cuando sea difícil explicar las relaciones, revise el diseño.
5. Trate los datos externos como desconocidos
Las respuestas de API, el almacenamiento, los datos de terceros y la entrada del usuario deben validarse en los límites antes de convertirse en objetos de dominio confiables.
6. Mida antes de culpar a TypeScript
Un CI lento o un servicio de traducción deficiente suelen reflejar la arquitectura de tipos, no la marca del lenguaje. Analice el perfil y luego simplifique los tipos más utilizados.
TypeScript debería hacer que el código sea aburrido
TypeScript está en su mejor forma cuando no interfiere: autocompletado, refactorizaciones seguras y menos sorpresas en tiempo de ejecución. No debería sentirse como un acertijo cada vez que se modifica un componente. Las interfaces menos impresionantes —objetos comerciales simples— suelen ser las más valiosas. Mantenga los tipos simples, prácticos y vinculados a conceptos reales del producto para que el equipo pueda entregar resultados en lugar de dedicarse a descifrar la álgebra de tipos.
Mantenga los tipos simples, prácticos y vinculados a conceptos reales del producto para que el equipo pueda entregar resultados en lugar de dedicarse a descifrar la álgebra de tipos.
Las retrospectivas mejoran cuando la conversación pasa de hablar del nombre de la marca al analizar las decisiones de diseño: cuántos tipos genéricos usar, qué nivel de derivación aplicar, cuán honesta es la máquina de estados y cómo ingresan los datos externos a la aplicación. TypeScript recompensa esa honestidad con una retroalimentación más rápida sobre los cambios que realmente importan. Por el contrario, castiga la astucia con errores opacos. Elija intencionalmente el camino más sencillo, documente los pocos tipos avanzados que realmente son esenciales y evite incluir patrones complejos en las bibliotecas compartidas a las que deben acceder los ingenieros más nuevos desde el primer día.
Cuando sienta que un cambio se ve obstaculizado por los tipos, pregúntese si el modelo refleja realmente al producto. A menudo, la solución no radica en condiciones más complejas, sino en una interfaz más clara, un módulo separado o una unión que nombre los estados ya mencionados en las reuniones diarias. Así es como TypeScript deja de ser una carga adicional y vuelve a ser una herramienta útil.
Lecturas relacionadas
- Uniones discriminadas en TypeScript: Hacen que los estados ilegales sean imposibles de representar — Sustituya los conjuntos de estados booleanos por uniones discriminadas en TypeScript: verificación exhaustiva, tipos Result, cargas de archivos, autenticación, proceso de pago y propiedades que no pueden ser mal gestionadas.
- Diez hábitos de TypeScript que mantienen a las base de código grandes legibles y seguras — Aprenda diez hábitos prácticos de TypeScript, desde generics significativos y reducción de opciones hasta verificaciones exhaustivas, atributos readonly y una configuración tsconfig estricta, que permiten mantener actualizadas las bases de código en constante crecimiento.