Introducción a TypeScript para desarrolladores JavaScript
Guía completa de TypeScript con tipos avanzados, generics, decoradores y estrategias de migración empresarial usadas por Airbnb, Slack y Microsoft.
¿Qué es TypeScript?
TypeScript es un superconjunto de JavaScript que añade tipado estático opcional al lenguaje. Desarrollado y mantenido por Microsoft, TypeScript se compila a JavaScript puro. Empresas como Airbnb, Slack, Asana y Google (Angular) han migrado sus codebases a TypeScript, reportando reducciones de hasta 15% en bugs de producción.
Ventajas de TypeScript
1. Detección temprana de errores
El sistema de tipos de TypeScript puede detectar muchos errores comunes durante el desarrollo, antes de que el código se ejecute.
function sumar(a: number, b: number): number {
return a + b;
}
// ✅ Correcto
sumar(5, 10); // 15
// ❌ Error en tiempo de compilación
// Error: Argument of type 'string' is not assignable to parameter of type 'number'
sumar(5, "10");
2. Mejor autocompletado y IntelliSense
Los editores modernos pueden proporcionar mejor autocompletado y sugerencias cuando conocen los tipos de tus variables y funciones.
3. Refactorización más segura
Con tipos explícitos, las herramientas pueden ayudarte a refactorizar código con mayor confianza, encontrando todos los usos de una función o propiedad.
4. Documentación automática
Los tipos sirven como documentación viva del código.
// El tipo documenta qué espera la función
function crearUsuario(data: {
nombre: string;
email: string;
edad?: number;
}): Usuario {
// ...
}
Tipos básicos
// Tipos primitivos
let nombre: string = "Juan";
let edad: number = 30;
let activo: boolean = true;
let nulo: null = null;
let indefinido: undefined = undefined;
// Arrays
let numeros: number[] = [1, 2, 3];
let nombres: Array<string> = ["Ana", "Luis"];
// Tuplas (arrays con tipos fijos)
let persona: [string, number] = ["Juan", 30];
// Enum
enum Estado {
Pendiente = "PENDIENTE",
Completado = "COMPLETADO",
Cancelado = "CANCELADO"
}
let estadoPedido: Estado = Estado.Pendiente;
// Any (evitar cuando sea posible)
let cualquierCosa: any = "texto";
cualquierCosa = 123; // OK pero pierde type safety
// Unknown (más seguro que any)
let desconocido: unknown = "texto";
// desconocido.toUpperCase(); // ❌ Error
if (typeof desconocido === "string") {
desconocido.toUpperCase(); // ✅ OK después de type guard
}
// Never (funciones que nunca retornan)
function error(mensaje: string): never {
throw new Error(mensaje);
}
Interfaces y Types
// Interface
interface Usuario {
id: number;
nombre: string;
email: string;
edad?: number; // Propiedad opcional
readonly createdAt: Date; // Solo lectura
}
// Type alias
type ID = string | number;
type Estado = "pendiente" | "completado" | "cancelado";
// Interface vs Type
// Interfaces pueden extenderse
interface Admin extends Usuario {
permisos: string[];
}
// Types pueden usar union types
type Respuesta = Usuario | Error;
// Intersection types
type UsuarioConRol = Usuario & {
rol: "admin" | "user";
};
Generics
Los generics permiten crear componentes reutilizables que funcionan con múltiples tipos.
// Función genérica
function primero<T>(array: T[]): T | undefined {
return array[0];
}
const numeros = [1, 2, 3];
const primerNumero = primero(numeros); // tipo: number | undefined
const nombres = ["Ana", "Luis"];
const primerNombre = primero(nombres); // tipo: string | undefined
// Clase genérica
class Cola<T> {
private items: T[] = [];
enqueue(item: T): void {
this.items.push(item);
}
dequeue(): T | undefined {
return this.items.shift();
}
peek(): T | undefined {
return this.items[0];
}
get size(): number {
return this.items.length;
}
}
const colaNumeros = new Cola<number>();
colaNumeros.enqueue(1);
colaNumeros.enqueue(2);
// Interface genérica
interface Respuesta<T> {
data: T;
error?: string;
timestamp: Date;
}
type UsuarioRespuesta = Respuesta<Usuario>;
type ListaUsuariosRespuesta = Respuesta<Usuario[]>;
// Constraints en generics
interface ConId {
id: number;
}
function obtenerPorId<T extends ConId>(items: T[], id: number): T | undefined {
return items.find(item => item.id === id);
}
Utility Types
TypeScript incluye varios utility types para transformar tipos existentes.
interface Usuario {
id: number;
nombre: string;
email: string;
edad: number;
password: string;
}
// Partial: hace todas las propiedades opcionales
type UsuarioParcial = Partial<Usuario>;
// { id?: number; nombre?: string; ... }
// Required: hace todas las propiedades requeridas
type UsuarioRequerido = Required<UsuarioParcial>;
// Readonly: hace todas las propiedades de solo lectura
type UsuarioReadonly = Readonly<Usuario>;
// Pick: selecciona propiedades específicas
type UsuarioPublico = Pick<Usuario, "id" | "nombre" | "email">;
// { id: number; nombre: string; email: string; }
// Omit: omite propiedades específicas
type UsuarioSinPassword = Omit<Usuario, "password">;
// Record: crea objeto con claves y valores específicos
type UsuariosPorId = Record<number, Usuario>;
// { [id: number]: Usuario }
// ReturnType: extrae tipo de retorno de función
function crearUsuario() {
return { id: 1, nombre: "Juan" };
}
type UsuarioCreado = ReturnType<typeof crearUsuario>;
// Parameters: extrae tipos de parámetros
type CrearUsuarioParams = Parameters<typeof crearUsuario>;
Type Guards y Type Narrowing
// typeof guard
function procesar(valor: string | number) {
if (typeof valor === "string") {
return valor.toUpperCase(); // TypeScript sabe que es string
}
return valor.toFixed(2); // TypeScript sabe que es number
}
// instanceof guard
class Perro {
ladrar() { console.log("Guau!"); }
}
class Gato {
maullar() { console.log("Miau!"); }
}
function hacerSonido(animal: Perro | Gato) {
if (animal instanceof Perro) {
animal.ladrar();
} else {
animal.maullar();
}
}
// Custom type guard
interface Pez {
nadar: () => void;
}
interface Ave {
volar: () => void;
}
function esPez(animal: Pez | Ave): animal is Pez {
return (animal as Pez).nadar !== undefined;
}
function mover(animal: Pez | Ave) {
if (esPez(animal)) {
animal.nadar();
} else {
animal.volar();
}
}
// Discriminated unions
type Forma =
| { tipo: "circulo"; radio: number }
| { tipo: "rectangulo"; ancho: number; alto: number }
| { tipo: "triangulo"; base: number; altura: number };
function calcularArea(forma: Forma): number {
switch (forma.tipo) {
case "circulo":
return Math.PI * forma.radio ** 2;
case "rectangulo":
return forma.ancho * forma.alto;
case "triangulo":
return (forma.base * forma.altura) / 2;
}
}
Decoradores (Experimental)
Usados por frameworks como NestJS, Angular y TypeORM.
// tsconfig.json: "experimentalDecorators": true
// Decorador de clase
function Loggeable(constructor: Function) {
console.log(`Clase ${constructor.name} creada`);
}
@Loggeable
class Usuario {
constructor(public nombre: string) {}
}
// Decorador de método
function Log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const metodoOriginal = descriptor.value;
descriptor.value = function(...args: any[]) {
console.log(`Llamando a ${propertyKey} con args:`, args);
const resultado = metodoOriginal.apply(this, args);
console.log(`Resultado:`, resultado);
return resultado;
};
}
class Calculadora {
@Log
sumar(a: number, b: number): number {
return a + b;
}
}
// Ejemplo con NestJS
@Controller('usuarios')
export class UsuariosController {
@Get()
@UseGuards(AuthGuard)
async obtenerTodos(): Promise<Usuario[]> {
return await this.usuariosService.findAll();
}
@Post()
@UsePipes(ValidationPipe)
async crear(@Body() dto: CrearUsuarioDto): Promise<Usuario> {
return await this.usuariosService.create(dto);
}
}
Estrategias de migración empresarial
1. Migración gradual (Recomendado)
// tsconfig.json
{
"compilerOptions": {
"allowJs": true, // Permite archivos .js
"checkJs": false, // No verifica archivos .js
"strict": false, // Desactiva modo estricto inicialmente
"noImplicitAny": false // Permite any implícito
}
}
Proceso:
- Renombrar
.jsa.tsgradualmente - Agregar tipos básicos
- Habilitar
strictpor módulo - Refactorizar con tipos avanzados
2. Usar @ts-check en archivos JS
// @ts-check
/**
* @param {number} a
* @param {number} b
* @returns {number}
*/
function sumar(a, b) {
return a + b;
}
3. Crear archivos de definición de tipos
// types/legacy.d.ts
declare module 'legacy-library' {
export function oldFunction(param: string): number;
}
Mejores prácticas empresariales
1. Usar strict mode
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictPropertyInitialization": true
}
}
2. Evitar any
// ❌ Malo
function procesar(data: any) {
return data.value;
}
// ✅ Bueno
function procesar<T extends { value: unknown }>(data: T) {
return data.value;
}
// ✅ O usar unknown
function procesar(data: unknown) {
if (typeof data === "object" && data !== null && "value" in data) {
return (data as { value: unknown }).value;
}
}
3. Usar tipos de retorno explícitos
// ✅ Bueno: tipo de retorno explícito
async function obtenerUsuario(id: number): Promise<Usuario | null> {
const usuario = await db.usuarios.findById(id);
return usuario;
}
4. Preferir interfaces para objetos públicos
// ✅ Interfaces para APIs públicas
export interface ConfiguracionAPI {
baseURL: string;
timeout: number;
headers?: Record<string, string>;
}
// Types para unions y aliases
type Estado = "activo" | "inactivo" | "suspendido";
Caso de estudio: Airbnb
Airbnb migró su codebase de JavaScript a TypeScript y reportó:
- 38% reducción en bugs que llegaron a producción
- 15% mejora en velocidad de desarrollo
- Mejor onboarding de nuevos desarrolladores
Conclusión
TypeScript ha sido adoptado por empresas líderes como:
- Microsoft: Creadores de TypeScript
- Google: Angular está escrito en TypeScript
- Airbnb: Migró toda su aplicación web
- Slack: Reescribió su aplicación de escritorio
- Asana: Migró gradualmente su frontend
Los beneficios incluyen:
- Menos bugs: Detección temprana de errores
- Mejor DX: Autocompletado y refactorización
- Escalabilidad: Código más mantenible
- Documentación: Los tipos documentan el código
TypeScript no es solo un lenguaje, es una inversión en la calidad y mantenibilidad de tu código a largo plazo.
Gustavo Leyva
Desarrollador de Software