Por que TypeScript?
TypeScript = JavaScript + tipos estaticos. Se compila a JS puro. No inventa un lenguaje nuevo — agrega una capa de seguridad que desaparece en runtime.
JavaScript es dinamico: una variable puede ser string ahora y number despues. Esto es flexible pero genera bugs silenciosos que solo aparecen en produccion. TypeScript detecta esos errores antes de ejecutar el codigo, en el editor, con lineas rojas y sugerencias.
function calcularTotal(items) { return items.reduce((s, i) => s + i.precio, 0); } calcularTotal("arrachera"); // TypeError: items.reduce is not a function // Error visible solo en produccion
interface Item { precio: number } function calcularTotal(items: Item[]) { return items.reduce((s, i) => s + i.precio, 0); } calcularTotal("arrachera"); // Error TS: Argument of type 'string' // not assignable to type 'Item[]'
npm install -D typescript ts-node && tsc --initEsto crea el tsconfig.json. Con
"strict": true activas todas las verificaciones mas utiles de golpe.Tipos Basicos
TS infiere tipos cuando puede. Solo anotas explicitamente cuando el compilador no puede inferir solo, o cuando quieres ser explicito por claridad.
// Primitivos con anotacion explicita const nombre: string = "Arrachera Premium"; const precio: number = 189.50; const activo: boolean = true; // Inferencia — TS deduce el tipo automaticamente const kilos = 2.5; // inferido: number const marca = "VLIM"; // inferido: string let stock = 42; // inferido: number // Arrays const precios: number[] = [189, 145, 98]; const tags: Array<string> = ["res", "cerdo"]; // Tuplas — longitud y tipos fijos const coord: [number, number] = [19.43, -99.13]; const fila: [string, number, boolean] = ["Bistec", 160, true]; // Enum — conjunto de valores nombrados enum Estado { Activo = "ACTIVO", Inactivo = "INACTIVO", Pendiente = "PENDIENTE", } const est: Estado = Estado.Activo; // unknown — alternativa segura a any let respuesta: unknown; if (typeof respuesta === "string") { console.log(respuesta.toUpperCase()); // ok — verificado } // Literal types — solo valores especificos type Moneda = "MXN" | "USD" | "EUR"; type MetodoHTTP = "GET" | "POST" | "PUT" | "DELETE"; const m: Moneda = "MXN"; // ok // const m2: Moneda = "BTC"; // Error: not assignable // never — funciones que nunca retornan function error(msg: string): never { throw new Error(msg); }
| Tipo | Ejemplo | Cuando usar |
|---|---|---|
| string | "hola" `texto ${v}` | Texto, nombres, IDs alfanumericos |
| number | 42 3.14 NaN | Precios, cantidades, indices |
| boolean | true false | Flags, estados binarios |
| null / undefined | null undefined | Valores opcionales — null para "sin valor" |
| unknown | JSON.parse(texto) | Datos de API externos — requiere narrowing antes de usar |
| never | throw new Error() | Funciones que siempre lanzan error |
| any | — evitar — | Solo como escape hatch temporal en migracion JS→TS |
unknown cuando no sabes el tipo — te obliga a verificar antes de usar.Interfaces y Type Aliases
Interface y type alias describen la "forma" de un objeto. Interface es preferida para objetos extensibles. type es mas versatil — puede ser union, interseccion o primitivo con alias.
// Interface — forma de un objeto interface Producto { id: string; nombre: string; precio: number; unidad: "kg" | "pieza" | "litro"; activo?: boolean; // ? = opcional stock: number; readonly creadoEn: string; // no se puede modificar } const arrachera: Producto = { id: "prod-001", nombre: "Arrachera Premium", precio: 189.50, unidad: "kg", stock: 42, creadoEn: "2024-01-15", }; // Extender interfaces interface ProductoConProveedor extends Producto { proveedorId: string; codigoBarras: string; fechaVence?: Date; } // Type alias — mas versatil type ID = string | number; type EstadoVenta = "pendiente" | "pagada" | "cancelada"; // Interseccion: combina dos tipos type Auditado = { creadoPor: string; creadoEn: Date; actualizadoEn: Date; }; type VentaAuditada = Venta & Auditado; // Index signatures — claves dinamicas interface PreciosPorSucursal { [sucursalId: string]: number; } const precios: PreciosPorSucursal = { "suc-01": 189, "suc-02": 195, }; // Diferencia clave: interface permite declaration merging interface Config { host: string } interface Config { port: number } // Config ahora tiene { host, port } fusionados // type no puede fusionarse — genera Duplicate identifier
Funciones Tipadas
Tipar los parametros y el retorno de una funcion es donde TypeScript da mas valor. El compilador verifica cada llamada y el editor autocompleta el resultado.
// Funcion basica con tipos function conIva(precio: number, iva: number = 0.16): number { return precio * (1 + iva); } // Arrow function tipada const formatMXN = (n: number): string => new Intl.NumberFormat("es-MX", { style: "currency", currency: "MXN" }).format(n); // Parametros opcionales y rest function crearEtiqueta(nombre: string, peso?: number, ...extras: string[]): string { let label = nombre; if (peso !== undefined) label += ` ${peso}kg`; if (extras.length) label += ` [${extras.join(", ")}]`; return label; } // Function overloads — misma funcion, firmas distintas function buscarProducto(id: string): Producto; function buscarProducto(codigo: number): Producto; function buscarProducto(query: string | number): Producto { if (typeof query === "string") return db.findById(query); return db.findByCodigo(query); } // Tipo de funcion como interfaz type CalculadoraPrecio = (precio: number, cantidad: number) => number; const calcularSubtotal: CalculadoraPrecio = (precio, cantidad) => precio * cantidad; const calcularDescuento: CalculadoraPrecio = (precio, pct) => precio * (pct / 100); // Higher-order functions tipadas function aplicarDescuento( fn: CalculadoraPrecio, precio: number, arg: number ): number { return precio - fn(precio, arg); } // Callback con tipos function procesarVentas( ventas: Venta[], onExito: (venta: Venta) => void, onError?: (err: Error, venta: Venta) => void ): void { ventas.forEach(v => { try { onExito(v); } catch (e) { onError?.(e as Error, v); } }); }
Generics — Tipos Parametrizados
Los generics permiten escribir codigo que funciona con cualquier tipo, manteniendo la seguridad de tipos. Como templates/plantillas: defines la logica una vez, el tipo se especifica al usar.
// Generic basico — <T> es el parametro de tipo function primero<T>(arr: T[]): T | undefined { return arr[0]; } primero([1, 2, 3]); // T = number primero(["a", "b"]); // T = string // Respuesta paginada generica — reutilizable para cualquier entidad interface PaginaRespuesta<T> { data: T[]; total: number; pagina: number; limite: number; siguiente?: string; } // Se usa con cualquier entidad const resp: PaginaRespuesta<Producto> = { data: [arrachera], total: 120, pagina: 1, limite: 20, }; // Constraints — limitar que tipos acepta el generic function obtenerNombre<T extends { nombre: string }>(entidad: T): string { return entidad.nombre; } obtenerNombre(arrachera); // ok — Producto tiene nombre obtenerNombre({ nombre: "Juan", rol: "admin" }); // ok // Generic con multiples parametros function mapear<T, R>(arr: T[], fn: (item: T) => R): R[] { return arr.map(fn); } const totales = mapear(ventas, v => v.total); // T=Venta, R=number // Clase generica — repositorio base del ERP class Repositorio<T extends { id: string }> { private items: Map<string, T> = new Map(); guardar(item: T): void { this.items.set(item.id, item); } obtener(id: string): T | undefined { return this.items.get(id); } listar(): T[] { return [...this.items.values()]; } eliminar(id: string): boolean { return this.items.delete(id); } } const productos = new Repositorio<Producto>(); const usuarios = new Repositorio<Usuario>();
Union Types y Narrowing
Un union type acepta varios tipos posibles. Narrowing es el proceso de reducir el tipo posible mediante verificaciones, permitiendo acceder a propiedades especificas de forma segura.
// Union type — puede ser cualquiera de los tipos type EntradaBusqueda = string | number | string[]; function buscar(query: EntradaBusqueda) { if (typeof query === "string") { return db.buscarPorNombre(query); // narrowed: string } if (typeof query === "number") { return db.buscarPorId(query); // narrowed: number } return db.buscarMultiples(query); // narrowed: string[] } // Discriminated unions — patron muy comun en ERP type EventoVenta = | { tipo: "creada"; venta: Venta; sucursalId: string } | { tipo: "cancelada"; ventaId: string; razon: string } | { tipo: "pagada"; ventaId: string; monto: number } | { tipo: "devuelta"; ventaId: string; items: string[] }; function manejarEvento(ev: EventoVenta): void { switch (ev.tipo) { case "creada": registrarVenta(ev.venta); // ev.venta disponible break; case "cancelada": cancelarVenta(ev.ventaId, ev.razon); // ev.razon disponible break; case "pagada": procesarPago(ev.ventaId, ev.monto); // ev.monto disponible break; default: const _exhaustive: never = ev; // Error si falta un caso } } // Type guards personalizados — is keyword function esProductoCarne(p: Producto): p is ProductoCarne { return ["res", "cerdo", "aves"].includes((p as ProductoCarne).categoria); } function procesarProducto(p: Producto) { if (esProductoCarne(p)) { console.log(p.categoria, p.corte); // narrowed: ProductoCarne } } // Nullish handling — muy comun con datos de BD function getNombreCliente(venta: Venta): string { return venta.cliente?.nombre ?? "Cliente mostrador"; } // Assertion functions — asegurar tipos en runtime function assertDefined<T>(val: T, msg: string): asserts val is NonNullable<T> { if (val === null || val === undefined) throw new Error(msg); }
never en el default.Utility Types
TypeScript incluye tipos de utilidad predefinidos para transformar tipos existentes. Evitan duplicar definiciones y hacen el codigo mas DRY.
interface Producto { id: string; nombre: string; precio: number; stock: number; activo: boolean; } // Partial<T> — todos los campos opcionales (para PATCH/update) type ActualizarProducto = Partial<Producto>; const update: ActualizarProducto = { precio: 195 }; // ok, resto opcional // Required<T> — todos los campos obligatorios type ProductoCompleto = Required<Producto>; // Readonly<T> — todo inmutable (respuestas de API) type ProductoAPI = Readonly<Producto>; // productoAPI.precio = 200; // Error: cannot assign to readonly // Pick<T, Keys> — seleccionar solo algunos campos type ProductoResumen = Pick<Producto, "id" | "nombre" | "precio">; type CrearProducto = Pick<Producto, "nombre" | "precio" | "stock">; // Omit<T, Keys> — excluir campos type ProductoSinId = Omit<Producto, "id">; type ProductoPublico = Omit<Producto, "stock" | "activo">; // Record<Keys, Type> — mapa tipado type StockPorSucursal = Record<string, number>; type PermisoPorRol = Record<"admin"|"cajero"|"bodeguero", string[]>; // Exclude / Extract — filtrar uniones type EstadoActivo = Exclude<EstadoVenta, "cancelada" | "devuelta">; // = "pendiente" | "pagada" // ReturnType / Parameters — extraer tipos de funciones type ResultadoCalculo = ReturnType<typeof calcularLinea>; type ParamsCalculadora = Parameters<typeof calcularLinea>; // NonNullable — elimina null y undefined de un tipo type ClienteDefinido = NonNullable<Venta["cliente"]>; // Awaited<T> (TS 4.5+) — tipo que devuelve una Promise type ProductoDB = Awaited<ReturnType<typeof obtenerProducto>>;
| Utility Type | Transformacion | Uso tipico en el ERP |
|---|---|---|
| Partial<T> | Todos opcionales | Body de PATCH, updates parciales |
| Required<T> | Todos obligatorios | Validacion antes de guardar en BD |
| Readonly<T> | Todos readonly | Respuestas de API, datos en cache |
| Pick<T, K> | Solo los campos K | DTOs, vistas de lista |
| Omit<T, K> | Sin los campos K | Input de creacion sin id/timestamps |
| Record<K, V> | Mapa K->V | Stock por sucursal, permisos por rol |
| ReturnType<F> | Tipo de retorno | Inferir tipo de resultado de servicio |
Clases Tipadas
TypeScript potencia las clases ES6 con modificadores de acceso, campos privados, implementacion de interfaces y tipos de retorno verificados por el compilador.
// Clase con modificadores de acceso class EntidadBase { readonly id: string; protected creadoEn: Date; private _version: number = 1; constructor(id?: string) { this.id = id ?? crypto.randomUUID(); this.creadoEn = new Date(); } get version(): number { return this._version; } toJSON(): Record<string, unknown> { return { id: this.id, creadoEn: this.creadoEn }; } } // Clase hija que implementa una interface interface IProducto { calcularPrecioFinal(cantidad: number): number; aplicarDescuento(pct: number): this; } class Producto extends EntidadBase implements IProducto { private _precio: number; private _descuento: number = 0; constructor( public readonly nombre: string, // shorthand: crea y asigna precio: number, public readonly unidad: string = "kg", ) { super(); this._precio = precio; } get precio(): number { return this._precio; } set precio(v: number) { if (v < 0) throw new Error("Precio no puede ser negativo"); this._precio = v; } calcularPrecioFinal(cantidad: number): number { const base = this._precio * cantidad * (1 - this._descuento); return base * 1.16; // IVA incluido } aplicarDescuento(pct: number): this { this._descuento = pct / 100; return this; // fluent API } static desdeBD(row: Record<string, unknown>): Producto { const p = new Producto(row.nombre as string, row.precio as number); return p; } } // Uso: fluent API const p = new Producto("Arrachera", 189) .aplicarDescuento(10); console.log(p.calcularPrecioFinal(2)); // 189*2*0.9*1.16
Funciones Tipadas
Tipar parametros y retorno de funciones es donde TypeScript da mas valor β el compilador verifica cada llamada en todo el proyecto.
// Parametros y retorno tipados function calcularTotal(precio: number, cantidad: number): number { return precio * cantidad; } // Parametros opcionales y con valor por defecto function formatPrecio( valor: number, moneda: string = "MXN", // default decimales?: number // opcional ): string { return new Intl.NumberFormat("es-MX", { style: "currency", currency: moneda, minimumFractionDigits: decimales ?? 2, }).format(valor); } // Arrow functions tipadas const conIVA = (precio: number): number => precio * 1.16; // Funcion que retorna void (no retorna valor util) const logVenta = (ventaId: string): void => { console.log(`Venta registrada: ${ventaId}`); }; // Tipado de funciones como valor (Function type) type Transformador = (valor: number) => number; const aplicarDescuento: Transformador = (v) => v * 0.9; // Sobrecarga de funciones (overloads) function buscarProducto(id: number): Producto; function buscarProducto(codigo: string): Producto; function buscarProducto(param: number | string): Producto { if (typeof param === "number") { return buscarPorId(param); } return buscarPorCodigo(param); } // Rest parameters tipados function sumarPrecios(...precios: number[]): number { return precios.reduce((a, b) => a + b, 0); } // Callbacks tipados function procesarItems( items: Producto[], callback: (item: Producto, index: number) => void ): void { items.forEach(callback); }
Generics β Tipos Reutilizables
Los generics permiten escribir codigo que funciona con cualquier tipo sin perder la seguridad. Son el mecanismo principal de reutilizacion en TypeScript.
// Generic basico β T es un "tipo parametro" function primerElemento<T>(array: T[]): T | undefined { return array[0]; } const p1 = primerElemento([1, 2, 3]); // inferido: number | undefined const p2 = primerElemento(["a", "b"]); // inferido: string | undefined // Respuesta API generica β patron muy usado en el ERP interface ApiResponse<T> { data: T; total?: number; pagina?: number; ok: boolean; error?: string; } type ProductosResponse = ApiResponse<Producto[]>; type VentaResponse = ApiResponse<Venta>; // Restricciones con extends interface ConId { id: string } function buscarPorId<T extends ConId>(items: T[], id: string): T | undefined { return items.find(item => item.id === id); } // Funciona con cualquier objeto que tenga .id buscarPorId(productos, "prod-001"); buscarPorId(ventas, "vta-042"); // Multiples parametros de tipo function mapeo<K extends string, V>(keys: K[], val: V): Record<K, V> { return Object.fromEntries(keys.map(k => [k, val])) as Record<K, V>; } // Clase generica β repositorio generico del ERP class Repositorio<T extends ConId> { private items: Map<string, T> = new Map(); guardar(item: T): void { this.items.set(item.id, item); } buscar(id: string): T | undefined { return this.items.get(id); } todos(): T[] { return [...this.items.values()]; } } const repoProductos = new Repositorio<Producto>(); const repoVentas = new Repositorio<Venta>();
Union Types y Narrowing
Un union type es "puede ser A o B". El narrowing es como TS detecta cual de los tipos es en cada rama del codigo β usando typeof, instanceof, discriminant o type guards.
// ββ Union types βββββββββββββββββββββββββββββββββββββββ type StringOrNumber = string | number; type NullableString = string | null; // ββ typeof narrowing ββββββββββββββββββββββββββββββββββ function formatId(id: string | number): string { if (typeof id === "number") { return id.toString().padStart(6, "0"); // TS sabe: id es number aqui } return id.toUpperCase(); // TS sabe: id es string aqui } // ββ Discriminated unions β patron clave del ERP βββββββ interface PagoEfectivo { tipo: "efectivo"; monto: number; } interface PagoTarjeta { tipo: "tarjeta"; monto: number; ultimos4: string; autorizacion:string; } interface PagoTransferencia { tipo: "transferencia"; monto: number; clabe: string; banco: string; } type Pago = PagoEfectivo | PagoTarjeta | PagoTransferencia; function procesarPago(pago: Pago): string { switch (pago.tipo) { case "efectivo": return `Efectivo: $${pago.monto}`; case "tarjeta": return `Tarjeta ***${pago.ultimos4} auth:${pago.autorizacion}`; case "transferencia": return `SPEI ${pago.banco} CLABE:${pago.clabe}`; default: const _exhaustive: never = pago; // error si falta un caso return _exhaustive; } } // ββ Type guards personalizados ββββββββββββββββββββββββ function esPagoTarjeta(pago: Pago): pago is PagoTarjeta { return pago.tipo === "tarjeta"; } // ββ Nullish narrowing βββββββββββββββββββββββββββββββββ function obtenerNombre(p: Producto | null): string { if (!p) return "Sin producto"; return p.nombre; // TS sabe: p no es null aqui } // ββ Optional chaining + nullish coalescing ββββββββββββ const ciudad = cliente?.direccion?.ciudad ?? "Sin ciudad";
Utility Types
TS incluye tipos de utilidad que transforman tipos existentes. Evitan duplicar definiciones y son esenciales en APIs REST β DTO de creacion, actualizacion parcial, solo lectura.
interface Producto { id: string; nombre: string; precio: number; stock: number; activo: boolean; creadoEn: Date; } // Partial β todas las propiedades opcionales (PATCH/update) type ActualizarProducto = Partial<Producto>; // { id?: string; nombre?: string; precio?: number; ... } // Required β todas obligatorias (inversion de Partial) type ProductoCompleto = Required<Producto>; // Readonly β ninguna propiedad modificable type ProductoFijo = Readonly<Producto>; // Pick β seleccionar solo algunas propiedades type ProductoLista = Pick<Producto, "id" | "nombre" | "precio">; // { id: string; nombre: string; precio: number } // Omit β excluir algunas propiedades (DTO de creacion) type CrearProducto = Omit<Producto, "id" | "creadoEn">; // { nombre: string; precio: number; stock: number; activo: boolean } // Record β objeto con claves y valores tipados type StockPorSucursal = Record<string, number>; type CacheProductos = Record<string, Producto>; const stock: StockPorSucursal = { "suc-01": 42, "suc-02": 18, "suc-03": 5 }; // Exclude / Extract β filtrar unions type EstadoVenta = "pendiente" | "pagada" | "cancelada" | "devuelta"; type EstadoFinal = Extract<EstadoVenta, "pagada" | "cancelada">; // "pagada" | "cancelada" type EstadoActivo = Exclude<EstadoVenta, "cancelada" | "devuelta">; // "pendiente" | "pagada" // NonNullable β elimina null y undefined type ClienteSeguro = NonNullable<Cliente | null | undefined>; // Cliente // ReturnType β extrae el tipo de retorno de una funcion function crearVenta() { return { id: "", total: 0, items: [] }; } type Venta = ReturnType<typeof crearVenta>; // Parameters β extrae parametros de una funcion type ParamsCalcular = Parameters<typeof calcularTotal>; // [precio: number, cantidad: number] // Awaited β extrae el tipo resuelto de una Promise (TS 4.5+) type ResultadoDB = Awaited<ReturnType<typeof buscarProductoAsync>>;
| Utility Type | Uso tipico en ERP |
|---|---|
| Partial<T> | DTO de actualizacion parcial (PATCH endpoints) |
| Omit<T, K> | DTO de creacion (sin id ni timestamps) |
| Pick<T, K> | Proyecciones para listas (solo campos necesarios) |
| Record<K, V> | Cache de productos, stock por sucursal, mapas |
| ReturnType<F> | Tipar resultados de servicios sin duplicar interfaces |
| Awaited<T> | Extraer el tipo resuelto de promesas de BD |
Clases Tipadas
Las clases TS amplian las clases JS con modificadores de acceso (public/private/protected), campos privados verdaderos con #, propiedades readonly y herencia fuertemente tipada.
abstract class EntidadBase { readonly id: string; readonly creadoEn: Date; actualizadoEn: Date; constructor(id?: string) { this.id = id ?? crypto.randomUUID(); this.creadoEn = new Date(); this.actualizadoEn = new Date(); } abstract validar(): boolean; // subclases deben implementar toJSON(): Record<string, unknown> { return { id: this.id, creadoEn: this.creadoEn }; } } class Producto extends EntidadBase { // Campo privado β verdaderamente inaccesible fuera (ES2022) #stockInterno: number; constructor( public readonly nombre: string, // shorthand: crea y asigna en constructor public precio: number, private codigoBarra: string, stockInicial: number = 0 ) { super(); this.#stockInterno = stockInicial; } // Getter/setter tipados get stock(): number { return this.#stockInterno; } set stock(cantidad: number) { if (cantidad < 0) throw new Error("Stock no puede ser negativo"); this.#stockInterno = cantidad; this.actualizadoEn = new Date(); } precioConIVA(): number { return this.precio * 1.16; } static desdeBD(row: Record<string, unknown>): Producto { const p = new Producto( row.nombre as string, row.precio as number, row.codigo_barra as string, row.stock as number ); return p; } validar(): boolean { return this.nombre.length > 0 && this.precio > 0; } } // Interfaces para clases β contrato publico interface IServicio<T> { buscarPorId(id: string): Promise<T | null>; listar(filtros?: Partial<T>): Promise<T[]>; crear(datos: Omit<T, "id">): Promise<T>; actualizar(id: string, datos: Partial<T>): Promise<T>; eliminar(id: string): Promise<void>; } class ProductosService implements IServicio<Producto> { // TS verifica que implementa TODOS los metodos de la interfaz async buscarPorId(id: string): Promise<Producto | null> { return null; } async listar(): Promise<Producto[]> { return []; } async crear(datos: Omit<Producto, "id">): Promise<Producto> { return datos as Producto; } async actualizar(id: string, datos: Partial<Producto>): Promise<Producto> { return datos as Producto; } async eliminar(id: string): Promise<void> { } }
Modulos y tsconfig
tsconfig.json controla como TS compila tu codigo. Con strict: true activas 8 verificaciones que previenen la mayoria de bugs comunes. La configuracion correcta es la base del proyecto.
{
"compilerOptions": {
// Target y formato de salida
"target": "ES2022",
"module": "NodeNext", // ESM nativo de Node 20
"moduleResolution": "NodeNext",
"outDir": "./dist",
"rootDir": "./src",
// Strict mode β SIEMPRE activar en proyectos nuevos
"strict": true, // activa los 8 de abajo
"noImplicitAny": true, // parametros sin tipo -> error
"strictNullChecks": true, // null/undefined separados
"strictFunctionTypes": true,
"strictPropertyInitialization": true, // propiedades inicializadas
"noUncheckedIndexedAccess": true, // array[i] puede ser undefined
// Calidad extra
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"exactOptionalPropertyTypes": true,
// Utilidades
"sourceMap": true,
"declaration": true, // genera .d.ts
"esModuleInterop":true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true, // import config from './config.json'
// Para path aliases (@/services/...)
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
{
"scripts": {
"build": "tsc",
"dev": "tsx watch src/index.ts", // tsx: ts-node alternativa mas rapida
"typecheck": "tsc --noEmit", // solo verificar, no compilar
"start": "node dist/index.js"
},
"devDependencies": {
"typescript": "^5.4.0",
"tsx": "^4.0.0", // ejecuta TS directamente en Node
"@types/node":"^20.0.0" // tipos de Node.js
}
}
Async Tipado
Promise y async/await en TypeScript llevan el tipo del valor resuelto. Promise<Producto> garantiza que al hacer await siempre obtienes un Producto β no any.
// Promise tipada β T es el tipo resuelto async function buscarProducto(id: string): Promise<Producto | null> { const row = await db.query('SELECT * FROM productos WHERE id=$1', [id]); return row ? Producto.desdeBD(row) : null; } async function listarProductos(filtros?: { activo?: boolean; limite?: number; offset?: number; }): Promise<{ data: Producto[]; total: number }> { const { activo = true, limite = 20, offset = 0 } = filtros ?? {}; const [data, total] = await Promise.all([ db.queryRows<Producto>('SELECT * FROM productos WHERE activo=$1 LIMIT $2 OFFSET $3', [activo, limite, offset]), db.queryOne<{ count: number }>('SELECT COUNT(*) FROM productos WHERE activo=$1', [activo]), ]); return { data, total: total?.count ?? 0 }; } // Result type β alternativa a try/catch en toda la cadena type Ok<T> = { ok: true; data: T }; type Err<E> = { ok: false; error: E }; type Result<T, E = string> = Ok<T> | Err<E>; async function registrarVenta(dto: CrearVenta): Promise<Result<Venta>> { try { const venta = await ventasService.crear(dto); return { ok: true, data: venta }; } catch (e) { return { ok: false, error: e instanceof Error ? e.message : "Error desconocido" }; } } // Uso con narrowing automatico const resultado = await registrarVenta(dto); if (resultado.ok) { console.log(resultado.data.id); // TS sabe: data es Venta } else { console.error(resultado.error); // TS sabe: error es string } // Tipar respuestas de APIs externas con zod (validacion en runtime) // import { z } from 'zod'; // const ProductoSchema = z.object({ id: z.string(), precio: z.number() }); // type ProductoAPI = z.infer<typeof ProductoSchema>;
Patrones Avanzados
Mapped types, conditional types y template literal types son las herramientas de metaprogramacion de TypeScript β generan tipos a partir de otros tipos automaticamente.
// ββ Mapped types β transformar cada propiedad βββββββββ type Nullable<T> = { [K in keyof T]: T[K] | null }; type Getters<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K] }; // Template Literal Types β generar nombres de eventos type Entidad = "venta" | "producto" | "cliente"; type Accion = "creado" | "actualizado" | "eliminado"; type Evento = `${Entidad}.${Accion}`; // "venta.creado" | "venta.actualizado" | ... | "cliente.eliminado" function onEvento(evento: Evento, handler: () => void): void { /* ... */ } onEvento("venta.creado", handler); // ok // onEvento("sucursal.creado", handler); // Error β no es Evento valido // Conditional types β tipo que depende de una condicion type IsArray<T> = T extends any[] ? true : false; type Unwrap<T> = T extends Promise<infer U> ? U : T; // Unwrap<Promise<Producto>> β Producto // Unwrap<string> β string // keyof y typeof β reflexion de tipos const CONFIG = { host: "localhost", port: 5432, ssl: false } as const; type ConfigKey = keyof typeof CONFIG; // "host" | "port" | "ssl" type ConfigValue = (typeof CONFIG)[ConfigKey]; // string | number | boolean // Satisfies β verifica tipo sin perder informacion const roles = { admin: ["ventas.crear", "productos.editar", "reportes.ver"], cajero: ["ventas.crear"], gerente: ["reportes.ver", "ventas.crear"], } satisfies Record<string, string[]>; roles.admin // TS sabe que es string[], no solo string[] roles.cajero // acceso seguro por clave literal // Decoradores (experimental, Moleculer los usa con reflect-metadata) // @Service({ name: "productos" }) // class ProductosService { ... }
TypeScript en el ERP CarnicerΓas
El dominio real del ERP tipado de principio a fin: entidades, DTOs, servicios y eventos. Esto es lo que el compilador verifica en cada commit.
// ββ types/dominio.ts β fuente unica de verdad βββββββββ export type ID = string; export type Dinero = number; // siempre en pesos MXN export type Kilos = number; export interface Auditado { creadoEn: Date; actualizadoEn: Date; creadoPor: ID; } export interface Producto extends Auditado { id: ID; nombre: string; codigo: string; precio: Dinero; unidad: "kg" | "pieza" | "litro"; categoria: "res" | "cerdo" | "pollo" | "embutido" | "otro"; perecedero: boolean; activo: boolean; } export interface LineaVenta { productoId: ID; nombre: string; cantidad: number; precio: Dinero; descuento: number; // 0-1 subtotal: Dinero; iva: Dinero; total: Dinero; } export type MetodoPago = "efectivo" | "tarjeta" | "transferencia" | "credito"; export type EstadoVenta = "abierta" | "pagada" | "cancelada" | "devuelta"; export interface Venta extends Auditado { id: ID; sucursalId: ID; clienteId?: ID; items: LineaVenta[]; subtotal: Dinero; descuentos: Dinero; iva: Dinero; total: Dinero; metodoPago: MetodoPago; estado: EstadoVenta; folio: string; cfdiUUID?: string; } // ββ DTOs de entrada βββββββββββββββββββββββββββββββββββ export type CrearVentaDTO = Pick<Venta, "sucursalId" | "clienteId" | "metodoPago" > & { items: Array<{ productoId: ID; cantidad: number; descuento?: number; }>; }; // ββ Servicio tipado con Result βββββββββββββββββββββββββ type Result<T> = { ok: true; data: T } | { ok: false; error: string; code: number }; export interface IVentasService { registrar(dto: CrearVentaDTO): Promise<Result<Venta>>; cancelar(id: ID, motivo: string): Promise<Result<Venta>>; listar(filtros: { sucursalId?: ID; fechaDesde?: Date; fechaHasta?: Date; estado?: EstadoVenta; pagina?: number; limite?: number; }): Promise<{ data: Venta[]; total: number }>; } // ββ Eventos tipados para Moleculer ββββββββββββββββββββ export type EventoVenta = | { tipo: "venta.registrada"; venta: Venta } | { tipo: "venta.cancelada"; ventaId: ID; motivo: string } | { tipo: "venta.cfdi.emitido"; ventaId: ID; uuid: string };