Tag: ZOD

  • Asegura el tipo de datos en function calling usando TypeScript

    Asegura el tipo de datos en function calling usando TypeScript

    Function calling tipado con TypeScript: deja de adivinar lo que devuelve el modelo

    Tiempo estimado de lectura: 4 min

    • Ideas clave:
    • Los LLMs fallan en formato y semántica: validar la salida evita errores en producción.
    • Define esquemas con Zod, deriva tipos con z.infer<>, y valida antes de ejecutar herramientas.
    • Usa .parse() para fallar rápido en endpoints y .safeParse() para autocorrección en agentes.
    • Mide y registra: trazabilidad completa (prompt, response, error de Zod, tool invocada).

    Introducción

    Cuando un agente llama a una herramienta, el modelo genera un JSON con argumentos. Asumir que ese JSON tendrá la forma correcta es la fuente de la mayoría de fallos en producción. Implementar Function calling tipado con TypeScript: deja de adivinar lo que devuelve el modelo no es opcional: es ingeniería defensiva. Con Zod validas en runtime, con z.infer<> obtienes tipos sincronizados y con un framework que integre ambos cierras el círculo.

    Fuentes útiles: Vercel AI SDK, Zod, OpenAI Structured Outputs.

    Resumen rápido (lectores con prisa)

    Qué es: Validación tipada de la salida de modelos mediante Zod y TypeScript.

    Cuándo usarlo: Siempre que un LLM invoque herramientas, modifique estado o llame APIs críticas.

    Por qué importa: Previene errores por campos faltantes, tipos incorrectos o JSON mal formado en producción.

    Cómo funciona: Define esquemas Zod, deriva tipos con z.infer<>, valida con .parse() o .safeParse() antes de ejecutar.

    Por qué tipar el function calling importa ahora

    Los LLMs fallan de formas predecibles: omiten campos, envían strings en vez de números, rodean JSON con Markdown o inventan claves. Si procesas ese output con JSON.parse() y as Tipo, renuncias a la seguridad de TypeScript en runtime. El resultado: escrituras corruptas en bases de datos, llamadas a APIs con parámetros inválidos y bugs que sólo aparecen semanas después.

    La alternativa técnica es clara:

    • declarar el esquema con Zod,
    • derivar el tipo TypeScript con z.infer<>,
    • validar antes de ejecutar la herramienta.

    Eso convierte la entrada del agente en un contrato matemático que protege tu lógica de negocio.

    Arquitectura práctica: esquema → validación → ejecución

    Patrón recomendado:

    1. Define el esquema Zod y añádele descripciones que el LLM pueda leer.
    2. Expón ese esquema en el prompt (o úsalo con Structured Outputs).
    3. Valida la respuesta del LLM con .safeParse() o .parse() antes de llamar a la función.
    4. Si falla, captura el ZodError, loguéalo y opcionalmente reintenta con autocorrección.

    Código mínimo (ejemplo de consulta de divisas)

    import { tool } from 'ai'; // p. ej. Vercel AI SDK
    import { z } from 'zod';
    
    const ExchangeSchema = z.object({
      base: z.string().length(3).toUpperCase().describe('Moneda base ISO 4217, ej. USD'),
      target: z.string().length(3).toUpperCase().describe('Moneda destino ISO 4217, ej. EUR'),
    });
    
    type ExchangeParams = z.infer;
    
    export const getExchangeRate = tool({
      description: 'Devuelve el tipo de cambio entre dos monedas',
      parameters: ExchangeSchema,
      execute: async ({ base, target }: ExchangeParams) => {
        const res = await fetch(`https://api.exchangerate-api.com/v4/latest/${base}`);
        if (!res.ok) throw new Error('API externa falló');
        const data = await res.json();
        return { rate: data.rates[target] };
      }
    });
    

    Si la validación falla, execute nunca se ejecuta: el SDK/Zod detiene la cadena y devuelve un error estructurado.

    .parse() vs .safeParse() y autocorrección

    Usa .parse() cuando quieras fallar rápido (endpoints HTTP que deben devolver 4xx/5xx). Usa .safeParse() en agentes y workflows que puedan auto‑corregirse sin intervención humana.

    Patrón de autocorrección:

    1. LLM genera JSON.
    2. .safeParse() devuelve success: false y error.
    3. Serializas error.flatten() y lo inyectas en un nuevo prompt: “Tu respuesta falló por X. Corrige el JSON.”
    4. Reintentás N veces con backoff; si sigue fallando, encolas para revisión humana.

    Ese ciclo convierte errores estructurales en una conversación de corrección con el modelo, robusta y trazable.

    Operaciones y observabilidad

    No basta con validar: mide y actúa.

    Métricas recomendadas:

    • tasa de validación fallida por prompt/modelo,
    • latencia media de autocorrección,
    • número de reintentos hasta éxito,
    • porcentaje de degradaciones a intervención humana.

    Registra siempre: prompt, raw response, resultado de Zod (.error.flatten()), y el tool invocado. Eso te da trazabilidad: prompt → response → validación → acción. Sin esos registros no hay postmortem útil.

    Decisiones arquitectónicas y trade‑offs

    – Structured Outputs (OpenAI) y generateObject reducen errores de formato pero no sustituyen la validación semántica: un amount: -5 puede pasar el schema si no validas signo y rango. Siempre valida con Zod (https://zod.dev/).

    – Tipar desde el día 0 exige disciplina: los esquemas son contratos que obligan a diseñar prompts claros y a mantener tests de integración. La deuda que previene compensa la inversión inicial.

    – En entornos orquestados (n8n, XState) preferir que el LLM decida la herramienta y que la ejecución quede en una máquina de estado puede ser más seguro para acciones críticas. Igual aplica: la entrada debe validarse antes de actuar.

    Conclusión: deja de adivinar, empieza a garantizar

    Function calling tipado con TypeScript: deja de adivinar lo que devuelve el modelo — es una fórmula sencilla y comprobada: define el esquema (Zod), extrae el tipo (z.infer<>), valida antes de ejecutar y automatiza la corrección cuando tenga sentido. Esa disciplina transforma un LLM impredecible en un componente confiable de tu arquitectura. Si tu agente escribe en bases de datos, llama APIs facturadas o toma decisiones que afectan a clientes, no hay excusas: valida antes de ejecutar y loguea todo. Así se construyen agentes que pueden correr solos, y no problemas que sólo aparecen en producción.

    Para continuar explorando prácticas operativas y experimentos en automatización e IA aplicada, consulta Dominicode Labs. Esta referencia complementa las técnicas descritas y ofrece recursos prácticos para implementar pipelines seguros y trazables en producción.

    FAQ

    ¿Por qué no basta con hacer JSON.parse() y castear a un tipo?

    Porque JSON.parse() solo asegura formato JSON válido, no la semántica ni la presencia y tipo de campos esperados. Castear con as Tipo ignora la verificación en runtime, lo que permite entradas inválidas que pueden provocar errores en bases de datos o llamadas a APIs en producción.

    ¿Cuándo debo usar .parse() en lugar de .safeParse()?

    Usa .parse() en contextos donde quieras fallar rápido y retornar un error (por ejemplo endpoints HTTP que deben devolver 4xx/5xx). Usa .safeParse() cuando el flujo puede intentar autocorrección o reintentos antes de degradar a intervención humana.

    ¿Qué hago si .safeParse() falla continuamente?

    Serializa el error con error.flatten(), inyecta esa información en un nuevo prompt pidiendo corrección, y reintenta N veces con backoff. Si sigue fallando, encola la unidad para revisión humana y registra el incidente para análisis posterior.

    ¿Debo exponer el esquema Zod en el prompt?

    Sí: exponer el esquema ayuda al modelo a generar la estructura correcta (especialmente con Structured Outputs). Aun así, la validación con Zod debe ejecutarse en runtime; el esquema en el prompt no sustituye la verificación.

    ¿Qué debo registrar para tener trazabilidad adecuada?

    Registra el prompt, la respuesta cruda del modelo, el resultado de Zod (error.flatten()), y la herramienta (tool) invocada. Esos datos permiten reconstruir el flujo prompt → response → validación → acción para postmortems.

    ¿Los Structured Outputs sustituyen la validación con Zod?

    No. Structured Outputs y utilidades como generateObject reducen errores de formato, pero no validan semántica ni rangos (por ejemplo, amount: -5 podría pasar). Sigue validando con Zod en runtime.

  • Cómo evitar el uso de `any` en TypeScript y mejorar tu código

    Cómo evitar el uso de `any` en TypeScript y mejorar tu código

    ¿Sigues parcheando con any porque “es más rápido”? Felicidades: acabas de convertir a tu compilador en un cómplice silencioso de los bugs nocturnos.

    Tiempo estimado de lectura: 5 min

    • TypeScript no te salva por arte de magia: es una herramienta que hay que configurar y aplicar, no decoración del IDE.
    • No uses any como parche: usa unknown o validación runtime para datos externos.
    • Activa strict y prioriza validación en la frontera: tsconfig, linters, CI y validadores como Zod.

    Introducción

    Poca gente lo dice tan claro: TypeScript no te salva automáticamente. Si lo tratas como decoración del IDE, te dará una falsa sensación de seguridad. Y cuando las cosas se rompan en producción, nadie recordará quién puso ese as any a las tres de la mañana.

    Voy a ser directo. Esto es lo que rompe proyectos y cómo lo arreglas para que deje de romperlos.

    Resumen rápido (lectores con prisa)

    TypeScript proporciona tipos estáticos para detectar errores tempranos en desarrollo. No valida tipos en runtime; para datos externos hay que usar validación en la frontera (por ejemplo Zod) y mantener "strict": true en tsconfig. Evita any y el operador de aserción no-nula !; prefiere unknown, encadenamiento opcional y guard clauses. Integra linters y CI que ejecuten tsc --noEmit y pruebas de contratos para evitar deuda técnica silenciosa.

    Qué falla y cómo lo arreglas

    1) Deja de usar any como parche rápido

    any = apagar las comprobaciones. unknown = obligarte a pensar.

    Usa unknown cuando no conoces la forma de un dato. Forcear any es como cerrar los ojos y conducir a 140 km/h: puedes llegar, o no.

    Ejemplo idiota, pero real:

    // NO
    function process(payload: any) {
      console.log(payload.name.toUpperCase());
    }
    
    // SÍ
    function processSafe(payload: unknown) {
      if (typeof payload === 'object' && payload !== null && 'name' in payload) {
        console.log((payload as { name: string }).name.toUpperCase());
      }
    }
    

    No te apetece escribir esa comprobación ahora. Perfecto: pon una validación con Zod y delega la detección a la frontera.

    2) Activa strict. No negociable.

    Si tu tsconfig dice "strict": false estás firmando cheques a la deuda técnica.

    Síntomas: parámetros sin tipado, nulos que aparecen sin avisar, inicialización incompleta de clases. Solución simple: "strict": true y arreglar las fallas una por una. ¿Migración? Usa @ts-expect-error puntualmente. No rebajes la seguridad global.

    3) El operador ! es una mentira elegante

    usuario.address!.street parece limpio. Es una bomba.

    Mejor: encadenamiento opcional o guard clauses.

    • usuario.address?.street — seguro, devuelve undefined.
    • if (!usuario.address) return — claro, explícito.

    Si ven ! en un PR, que explique por qué. Si no puede explicar, rechaza el PR.

    4) as T NO valida datos externos

    const data = await res.json() as User es confiarle la vida a un string.

    En la frontera (red, persistencia, input externo), usa validación runtime. Zod, io-ts, AJV: cualquiera que genere el guard que puedas correr al recibir datos. Y sí: extrae el esquema a un único lugar para que el tipo y el validador sean la misma verdad.

    Ejemplo con Zod:

    import { z } from 'zod';
    const UserSchema = z.object({ id: z.string(), name: z.string() });
    type User = z.infer;
    
    const raw = await res.json();
    const user = UserSchema.parse(raw); // lanza si no concuerda
    

    5) La “gimnasia de tipos” rompe equipos

    Type Gymnastics — esos tipos de 40 líneas que solo el autor entiende — son deuda técnica disfrazada. Úsalos donde aporten valor: librerías, infra, utilities core. No en cada componente. Si un tipo necesita 30 minutos para entenderlo, lo estás usando mal.

    Buenas prácticas: alias cortos, nombres claros, ejemplos en comentarios y tests de tipos (tsd) para que no se rompa en silencio.

    Checklist operativo (copia y pega y aplica ya)

    • tsconfig.json: "strict": true
    • ESLint: activar reglas
      • @typescript-eslint/no-explicit-any: error
      • @typescript-eslint/strict-boolean-expressions: warn/error
    • Pre-commit: husky + lint-staged para bloquear any y !
    • CI: job que ejecuta tsc --noEmit y tests de contratos (Zod parse)
    • Boundary validation: todas las respuestas HTTP parseadas con Zod/io-ts
    • Tests de tipos con tsd en PRs críticos

    Snippet de CI (esqueleto GitHub Actions)

    name: Validate Types
    on: [pull_request]
    jobs:
      types:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v3
          - run: pnpm install
          - run: pnpm build # incluye tsc --noEmit
          - run: pnpm test:contracts # tests que hacen parse de esquemas Zod
    

    Cómo aplicarlo en equipos (cultura > herramientas)

    • Regla simple: todo dato que venga de fuera pasa por un esquemas.
    • PRs deben documentar la asunción: “Por qué este ! está justificado”.
    • Prohibe any en lint. No excepciones.
    • Revisión de tipos en pair programming para cambios complejos.
    • Añade tests e2e que verifiquen la integración real del esquema (no solo mocks).

    Errores típicos y la respuesta corta

    • “No activamos strict porque produce demasiados errores” → Activarlo e ir resolviendo; cada corrección es una deuda pagada.
    • “Validar en runtime es caro” → El coste es mínimo comparado con el tiempo perdido debugueando un null en producción.
    • “Los tipos complejos son elegantes” → Sí. Elegantes y peligrosos si nadie los entiende.

    Mini-guía de herramientas que te salvan la vida

    • Zod: validación runtime + inferencia de tipos. Ganador simple.
    • @typescript-eslint: reglas para prohibir any, !, etc.
    • tsd: tests de tipos que evitan roturas silenciosas.
    • husky + lint-staged: bloqueo en pre-commit de malas prácticas.
    • Sentry / logs: cuando la validación falla en producción, registra payload + endpoint.

    Métrica que deberías vigilar semanalmente

    • PRs con any encontrados: objetivo = 0.
    • Rechazos por ! sin justificación: objetivo = 0.
    • Número de validaciones runtime faltantes en endpoints críticos: objetivo = 0.

    Si tienes más de 1 en cualquiera, tienes trabajo de deuda técnica.

    Cierre sin azúcar: esto es disciplina, no postureo

    TypeScript te da herramientas para poner reglas fuertes y claras en el código. Sin disciplina, esas reglas se convierten en adorno. Cambiar la cultura es más duro que tocar tsconfig, pero mucho más rentable.

    ¿Quieres algo práctico para arrancar mañana? Te puedo enviar:

    • Un repo template con tsconfig, ESLint, husky, Zod y pruebas de contratos listas.
    • Un workflow de GitHub Actions que falla el CI si hay any o ! no justificado.
    • Un checklist PDF para code reviews centrado en tipado seguro.

    Respóndeme “Envíame el template” o “Quiero el workflow” y te lo paso listo. No es sexy. Es lo que evita que pases la noche arreglando prod por culpa de un as any. Esto no acaba aquí.

    FAQ

    ¿Por qué no debo usar any?

    Porque desactiva las comprobaciones de tipos y oculta errores que el compilador podría detectar. Usar any convierte el compilador en cómplice de bugs que aparecen en producción.

    ¿Cuándo usar unknown en lugar de any?

    Usa unknown cuando recibes datos sin estructura conocida. Obliga a validar o refinar el tipo antes de operar sobre el valor, evitando asunciones peligrosas.

    ¿Qué hace exactamente “strict”: true?

    Activa un conjunto de opciones de compilador que refuerzan la seguridad de tipos: strictNullChecks, noImplicitAny, strictBindCallApply, entre otras. Detecta parámetros sin tipado, nulos no manejados y problemas de inicialización.

    ¿Cómo valido respuestas HTTP correctamente?

    Usa validación runtime en la frontera con esquemas compartidos (por ejemplo Zod). Parseas el payload recibido con el esquema y manejas el error si no concuerda antes de propagar datos al resto de la aplicación.

    ¿Qué hacer si activar strict rompe demasiados archivos a la vez?

    Actívalo y corrige las fallas progresivamente. Para casos puntuales usa @ts-expect-error temporalmente, pero no como solución permanente.

    ¿Cómo evitar que los tipos complejos sean incomprensibles?

    Prefiere alias cortos y nombres claros, añade ejemplos y tests de tipos (tsd) y reserva tipos largos para librerías o infraestrutura donde el equipo esté alineado.

  • Cuándo usar Zod en lugar de TypeScript para validación en runtime

    Cuándo usar Zod en lugar de TypeScript para validación en runtime

    Zod vs TypeScript puro: cuándo usar validación en runtime

    ¿Confías en TypeScript para proteger tu app en producción? Deja de hacerlo. TypeScript es un analizador estático; su trabajo termina cuando el código se compila. Si quieres seguridad real en ejecución necesitas otra cosa. Aquí va la guía práctica: Zod vs TypeScript puro: cuándo usar validación en runtime.

    TypeScript ordena tu código. Zod protege tus fronteras. Úsalos juntos, no en guerra.

    Tiempo estimado de lectura: 4 min

    • TypeScript es un analizador estático: no protege en runtime.
    • Zod parsea en runtime y mantiene una sola fuente de verdad con z.infer.
    • Valida en las fronteras (endpoints, webhooks, env, uploads); confía en TypeScript dentro del dominio.
    • Mide impacto de rendimiento y evita validaciones redundantes en el core.

    ¿Confías en TypeScript para proteger tu app en producción? Deja de hacerlo. TypeScript es un analizador estático; su trabajo termina cuando el código se compila. Si quieres seguridad real en ejecución necesitas otra cosa. Aquí va la guía práctica: Zod vs TypeScript puro: cuándo usar validación en runtime.

    Resumen rápido (lectores con prisa)

    Qué es: Zod es una librería de validación y parsing en runtime; TypeScript es un sistema de tipos estático.

    Cuándo usarlo: Usa Zod en las fronteras (endpoints, webhooks, env, uploads); usa TypeScript dentro del dominio.

    Por qué importa: TypeScript sufre type erasure y no impide fallos en producción; Zod parsea y falla rápido.

    Cómo usarlo: Definir esquemas Zod, derivar tipos con z.infer y parsear payloads en los handlers.

    Por qué TypeScript no basta (y dónde duele más)

    TypeScript tiene un problema estructural: Type Erasure. Los tipos desaparecen al compilar. Eso significa que las aserciones (as T) son mentiras que el compilador acepta y la app paga en runtime.

    interface Usuario { id: number; email: string; }
    
    const datos = await respuesta.json() as Usuario;
    console.log(datos.email.toLowerCase()); // Boom si email no existe
    

    Si la API cambia, o devuelve HTML en un error 500, tu código explota. TypeScript no está ahí para detenerlo.

    Zod y el principio “Parse, don’t validate”

    Zod opera en runtime. Su filosofía es clara: no intentes “validar” por inercia; parsea y falla rápido.

    import { z } from 'zod';
    
    const UsuarioSchema = z.object({
      id: z.number(),
      email: z.string().email(),
    });
    
    type Usuario = z.infer;
    
    const res = await fetch(url);
    const raw = await res.json();
    const usuario = UsuarioSchema.parse(raw); // lanza si algo falla
    console.log(usuario.email.toLowerCase());
    

    Con z.infer mantienes una sola fuente de verdad: el esquema. Nada de duplicar interfaces y validadores.

    safeParse vs parse

    Si quieres manejo de errores controlado:

    const result = UsuarioSchema.safeParse(raw);
    if (!result.success) {
      // logging, métricas, respuesta 400 al cliente...
      throw new Error('Payload inválido');
    }
    const usuario = result.data;
    

    safeParse devuelve un objeto con success y error para flujos menos crudos.

    Dónde aplicar validación (regla práctica)

    No todo necesita Zod. La regla del arquitecto es simple:

    • Valida en runtime en las fronteras: entrada HTTP, webhooks, archivos subidos, variables de entorno, LocalStorage.
    • Confía en TypeScript dentro del dominio: funciones internas, paso de props entre componentes, mutaciones internas en stores tipados.

    Aplicar Zod en todas partes degrada rendimiento y readability. No validar en las fronteras te expone a fallos catastróficos.

    Casos prácticos y patterns

    1) Respuesta API (backend → frontend o backend → backend)

    • Parsea siempre antes de usar.
    • Registra el error y devuelve 400 o fallback claro.

    2) Webhooks / n8n / automatizaciones

    • Parsea y verifica firma si aplica.
    • Rechaza rápido para evitar procesar datos corruptos.

    3) Variables de entorno en arranque (ejemplo con Zod)

    const EnvSchema = z.object({
      DATABASE_URL: z.string().url(),
      NODE_ENV: z.enum(['development','production']),
    });
    const env = EnvSchema.parse(process.env);
    

    Arranque fallido = fallo visible y evitar estados inconsistentes.

    4) Formularios en frontend

    Integra Zod con React Hook Form para validar antes de mutar el estado o enviar al servidor.

    Coste y alternativas: cuándo preocuparse por rendimiento

    Zod añade CPU y peso al bundle. En la mayoría de apps esto es irrelevante frente a la estabilidad que gana tu producto. En sistemas de altísima demanda (millones de eventos por segundo) evalúa alternativas más ligeras o validaciones a medida.

    Alternativas emergentes existen, pero la elección debe basarse en mediciones. No te cases con una librería sin perf tests en tu caso real.

    Integración práctica: patrón recomendado

    1. Punto de entrada (API handler, webhook) → Zod parse
    2. Convertir a tipos con z.infer → pasar al core tipado con TypeScript
    3. Lógica interna → TypeScript puro, sin comprobaciones redundantes
    4. En el cliente, validar inputs críticos; en el servidor, validar todo lo externo

    Conclusión y pasos accionables

    • Zod y TypeScript no son excluyentes. TypeScript organiza tu código; Zod lo hace seguro en producción.
    • Si tienes que elegir hoy, empieza por defender las fronteras: añade validación Zod en endpoints y webhooks.
    • Usa z.infer para que el tipo estático derive del esquema y mide impacto.

    Recuerda: el compilador no vive en producción. Tú sí. Protege lo que importa.

    Prueba esto ahora: añade un esquema Zod a uno de tus endpoints y corre safeParse con un payload inesperado. Verás la diferencia. Esto no acaba aquí; la próxima nota tratará cómo estructurar esquemas Zod para versiones y migraciones sin romper consumidores.

    FAQ

    ¿TypeScript protege mi app en producción?

    No. TypeScript es un analizador estático y sus tipos desaparecen al compilar (type erasure). No impide que datos inválidos lleguen a runtime.

    ¿Qué es Zod y por qué usarlo?

    Zod es una librería de validación y parsing en runtime. Su filosofía es “parse, don’t validate”: parsea datos y falla rápido si no coinciden con el esquema, evitando errores silenciosos en producción.

    ¿Cuándo usar parse vs safeParse?

    Usa parse cuando quieras que el proceso lance inmediatamente y falle rápido. Usa safeParse para manejar errores de forma controlada (logging, métricas, respuesta 400) sin exceptions no controladas.

    ¿Dónde aplicar validación en mi arquitectura?

    Valida en las fronteras: endpoints HTTP, webhooks (p. ej. n8n), archivos subidos, variables de entorno, y LocalStorage. Dentro del dominio confía en TypeScript y evita validaciones redundantes.

    ¿Zod impacta tanto el rendimiento?

    Zod añade CPU y peso al bundle, pero en la mayoría de apps el coste es aceptable frente a la estabilidad que aporta. En sistemas de altísima demanda evalúa alternativas más ligeras y mide con perf tests.

    ¿Cómo integrar Zod con TypeScript sin duplicar tipos?

    Define esquemas Zod y deriva los tipos estáticos con z.infer. Así mantienes una sola fuente de verdad: el esquema.