Javascript
Typescript
desarrollo web
Tipos
Genéricos
Si ya conoces los tipos básicos de TypeScript, probablemente te hayas dado cuenta de algo, los tipos primitivos te llevan hasta cierto punto, pero cuando empiezas a construir aplicaciones reales, necesitas herramientas más potentes. Aquí es donde TypeScript realmente brilla.
Este artículo explora las características que hacen que TypeScript sea más que "JavaScript con tipos". Son las herramientas que usarás todos los días una vez que superes la fase de "declarar todo como string o number".
Antes de entrar en lo avanzado, un repaso rápido. TypeScript incluye todos los tipos primitivos de JavaScript más algunos extras:
1const message: string = "Hola TypeScript"; 2const count: number = 42; 3const isActive: boolean = true; 4 5// Arrays 6const numbers: number[] = [1, 2, 3]; 7 8// Objetos con interfaces 9interface User { 10 name: string; 11 age: number; 12 email?: string; // opcional 13} 14 15const user: User = { 16 name: "Ana", 17 age: 28 18};
Si estos conceptos te resultan nuevos, vale la pena revisar el artículo introductorio de TypeScript antes de continuar. Lo que viene a continuación asume que ya te sientes cómodo con estos fundamentos.
En el mundo real, las cosas rara vez son blanco o negro. Una variable no siempre es "solo un string" o "solo un número". A veces es "un string O un número". A veces es "esto Y aquello". Los tipos compuestos existen precisamente para esos casos.
Imagina que estás construyendo una función que maneja el estado de una petición HTTP. El estado podría ser 'loading', 'success', o 'error'. ¿Cómo lo tipas?
Podrías usar string
, pero eso permitiría cualquier string, incluyendo valores sin sentido como 'banana'. Lo que realmente quieres es decir: "esta variable puede ser uno de estos valores específicos, y nada más".
Loading...
Este es un union type. El operador |
significa "o". Es como decirle a TypeScript: "confía en mí, este valor solo puede ser una de estas cosas".
La belleza de los union types es que TypeScript te ayuda a manejar cada caso:
Loading...
Los union types no se limitan a strings literales. Pueden ser cualquier combinación de tipos:
Loading...
Aquí pasa algo interesante. TypeScript es lo suficientemente inteligente para entender que dentro del if
, value
debe ser un number
(porque acabas de verificarlo). Y en el else
, tiene que ser un string
. Esto se llama "type narrowing" y es una de las características más útiles de TypeScript.
Mientras los union types dicen "esto O aquello", los intersection types dicen "esto Y aquello". Se definen con &
:
Loading...
¿Por qué querrías hacer esto en lugar de simplemente crear una interface Person
con ambas propiedades? Porque a veces quieres combinar interfaces que vienen de diferentes lugares:
Loading...
Esto es especialmente útil cuando trabajas con APIs. Puedes tener interfaces base que representan patrones comunes (como "todo tiene un ID" o "todo tiene timestamps") y combinarlas según necesites.
Esta es probablemente una de las preguntas más frecuentes: "¿cuándo uso type
y cuándo uso interface
?"
La respuesta honesta es: para objetos simples, cualquiera funciona bien y la elección es mayormente estilística. Pero hay algunas diferencias prácticas:
Loading...
La regla general: Si estás definiendo la forma de un objeto, especialmente si planeas extenderlo después, usa interface
. Para union types, intersection types complejos, o tipos más avanzados, usa type
.
Pero honestamente, no pierdas mucho tiempo preocupándote por esto. La consistencia en tu proyecto es más importante que la elección específica.
Los tipos literales llevan la precisión al siguiente nivel. En lugar de decir "esto es un string", puedes decir "esto es exactamente este string":
Loading...
Este patrón es increíblemente común en código real. Piensa en todas las veces que has tenido un valor que solo puede ser uno de un conjunto limitado de opciones. Los tipos literales hacen que esas restricciones sean explícitas y verificables.
Los genéricos son probablemente lo más intimidante de TypeScript cuando empiezas. Ves código con <T>
por todos lados y piensas "¿qué demonios es eso?". Pero una vez que haces clic, te das cuenta de que son increíblemente útiles.
Imagina que necesitas una función que retorna el primer elemento de un array. Sin genéricos, tendrías que hacer esto:
Loading...
Esto es ridículo. Es la misma lógica tres veces. Y cada vez que necesites un nuevo tipo, tendrías que escribir otra función.
Los genéricos resuelven esto:
Loading...
Una función, infinitos tipos. El <T>
es como un parámetro, pero para tipos en lugar de valores. Le estás diciendo a TypeScript: "voy a pasarte un tipo cuando use esta función, y quiero que uses ese tipo para tipar tanto el parámetro como el retorno".
Por convención, la gente usa T
(de "Type"), pero puedes usar cualquier nombre. De hecho, nombres más descriptivos a veces ayudan:
Loading...
A veces quieres que tu genérico tenga ciertas propiedades. No cualquier tipo, sino tipos que cumplan con ciertos requisitos. Para eso usas extends
:
Loading...
Esto es súper útil cuando necesitas acceder a propiedades específicas dentro de la función genérica.
Otro patrón común es restringir a objetos:
Loading...
Aquí está donde los genéricos realmente brillan. Cuando trabajas con APIs, tiendes a tener patrones que se repiten:
Loading...
Este patrón aparece constantemente en código de producción. En lugar de duplicar la estructura de respuesta para cada tipo de datos, la defines una vez con un genérico y la reutilizas.
Otros patrones útiles:
Loading...
Si hay un tipo que usas la mayoría del tiempo, puedes establecerlo como default:
Loading...
Las funciones son el corazón de JavaScript, y tiparlas correctamente es fundamental para aprovechar TypeScript.
A veces una función puede funcionar con o sin ciertos parámetros:
Loading...
El ?
hace que el parámetro sea opcional. Pero hay que tener cuidado: los parámetros opcionales deben ir después de los obligatorios. TypeScript no te deja hacer trampa aquí.
Loading...
Otra opción es usar valores por defecto, que hace las cosas más explícitas:
Loading...
Loading...
Esto es especialmente útil para funciones de utilidad:
Loading...
Las funciones pueden ser valores, y esos valores necesitan tipos:
Loading...
Esto hace que tu código sea más flexible. En lugar de tener una función rígida que hace una sola cosa, tienes una función que acepta comportamiento como parámetro.
También puedes definir function types con interfaces:
Loading...
Las funciones async siempre retornan una Promise
, y TypeScript necesita saber qué tipo de dato está dentro de esa promesa:
Loading...
Fíjate en el : Promise<User>
. Está diciendo "esta función retorna una promesa que eventualmente contendrá un User".
Si la función async no retorna nada significativo, usa Promise<void>
:
Loading...
El manejo de errores también puede tiparse:
Loading...
Aquí estás diciendo explícitamente: "esta función puede retornar un User o null si algo sale mal".
Los callbacks son funciones que pasas a otras funciones, y necesitan ser tipados correctamente:
Loading...
En situaciones más complejas, como cuando trabajas con APIs, los callbacks se vuelven más sofisticados:
Loading...
Una buena configuración de TypeScript es como tener buenos cimientos en una casa. No es glamoroso, pero hace toda la diferencia. El archivo tsconfig.json
controla cómo TypeScript compila tu código. Aquí está una configuración sólida para empezar:
1{ 2 "compilerOptions": { 3 "target": "ES2020", 4 "module": "ESNext", 5 "moduleResolution": "node", 6 7 "outDir": "./dist", 8 "rootDir": "./src", 9 10 "lib": ["ES2020"], 11 12 "strict": true, 13 14 "esModuleInterop": true, 15 "allowSyntheticDefaultImports": true, 16 17 "skipLibCheck": true, 18 "forceConsistentCasingInFileNames": true 19 }, 20 "include": ["src/**/*"], 21 "exclude": ["node_modules", "dist"] 22}
Esto puede parecer mucho, pero cada opción tiene un propósito.
1{ 2 "compilerOptions": { 3 "strict": true 4 } 5}
Esta única línea activa todas las verificaciones estrictas de TypeScript. Es tentador desactivarla cuando empiezas porque hace que el compilador se queje de todo. Pero resiste la tentación. strict: true
es lo que hace que TypeScript realmente te proteja de bugs.
Cuando está activado, TypeScript te obliga a:
any
implícitamentenull
y undefined
explícitamente Loading...
Más allá de strict
, hay opciones que detectan problemas sutiles:
1{ 2 "compilerOptions": { 3 "noUnusedLocals": true, 4 "noUnusedParameters": true, 5 "noImplicitReturns": true, 6 "noFallthroughCasesInSwitch": true 7 } 8}
noUnusedLocals: Te avisa cuando declaras variables que nunca usas.
Loading...
noImplicitReturns: Asegura que todas las ramas de una función retornen un valor.
Loading...
Dependiendo de dónde corra tu código, necesitarás diferentes configuraciones:
Para Node.js:
1{ 2 "compilerOptions": { 3 "target": "ES2020", 4 "module": "CommonJS", 5 "lib": ["ES2020"], 6 "types": ["node"] 7 } 8}
1{ 2 "compilerOptions": { 3 "target": "ES2020", 4 "module": "ESNext", 5 "lib": ["ES2020", "DOM"] 6 } 7}
La diferencia clave está en lib
, que define qué APIs están disponibles. Si incluyes "DOM", TypeScript conoce cosas como document
y window
. Si incluyes "node", conoce process
y Buffer
.
En conclusión, typeScript puede parecer mucho al principio. Los genéricos son confusos. Los tipos compuestos se sienten innecesarios. Pero aquí está la verdad, cada una de estas características existe porque resuelve un problema real que encontrarás en código de producción. Los genéricos evitan duplicación. Los union types hacen que las restricciones sean explícitas. La configuración estricta previene bugs sutiles.
Con el tiempo, escribir TypeScript se volverá tan natural como escribir JavaScript, pero con una red de seguridad gigante. Cada error que el compilador atrapa es un bug que no llegó a producción. Cada tipo que defines es documentación viviente que nunca queda desactualizada. TypeScript no es perfecto. A veces es verboso. A veces te hace saltar aros innecesarios. Pero en el balance, hace que el código sea más seguro, más mantenible, y más fácil de entender. Y eso vale cada <T>
que escribas.