Sébastien TIMONER
Experto en desarrollo web y gestión de equipos técnicos, me especializo en la creación y optimización de soluciones digitales de alto rendimiento. Gracias a un profundo dominio de tecnologías modernas como React.js, Node.js, TypeScript, Symfony, Docker y FrankenPHP, garantizo el éxito de proyectos SaaS complejos, desde el diseño hasta la implementación, para empresas de diversos sectores.
Los Utility Types de TypeScript son tipos genéricos predefinidos que facilitan las transformaciones de tipos. Permiten crear nuevos tipos a partir de otros existentes, por ejemplo haciendo que ciertas propiedades sean opcionales o extrayendo el tipo de retorno de una función. Esta guía educativa, en tres niveles (principiante, intermedio, avanzado), presenta todos los principales Utility Types de TypeScript (como Partial, Required, Readonly, Pick, Omit, Record, Exclude, Extract, NonNullable, Parameters, ReturnType, InstanceType, ThisType, etc.), con para cada uno: una explicación de su utilidad, un ejemplo de uso en TypeScript y una comparación con un enfoque equivalente en Zod, una librería de validación en tiempo de ejecución. También discutiremos las diferencias filosóficas entre los Utility Types de TypeScript (sistema de tipos estático) y los esquemas de Zod (validación en tiempo de ejecución).
Para probar los fragmentos de código, puedes inicializar un proyecto TypeScript mínimo usando Bun (un ejecutor de JavaScript ultrarrápido que también funciona como gestor de paquetes). Asegúrate de tener Bun instalado y luego, en una carpeta vacía, ejecuta:
shell
Bun creará un package.json y un tsconfig.json por defecto. Luego puedes ejecutar un archivo TypeScript con bun run file.ts (Bun transpila TypeScript al vuelo).
¡Ya estamos listos para explorar los Utility Types!
TypeScript: El Utility Type Partial<T> construye un tipo a partir de un tipo existente T haciendo que todas sus propiedades sean opcionales. Es decir, cada propiedad de T se vuelve opcional (añadiendo ?). Esto te permite representar objetos parciales de ese tipo. Por ejemplo, si tienes un tipo User con campos obligatorios, Partial<User> te permite crear una versión donde estos campos pueden omitirse:
typescript
Aquí, Partial<User> tiene el tipo { name?: string; age?: number }. Esto te permite, por ejemplo, implementar una función de actualización parcial sin requerir todos los campos:
typescript
Puedes llamar a updateUser(existingUser, { age: 26 }) con un objeto parcial – TypeScript garantiza estáticamente que fieldsToUpdate solo contiene claves válidas de User.
Equivalente con Zod: Zod ofrece un método equivalente para hacer que un esquema de objeto sea parcial. El método .partial() aplicado a un esquema z.object hace que todas las claves sean opcionales, similar a Partial en TypeScript. Por ejemplo:
typescript
Aquí PartialUserSchema es un esquema que valida objetos que pueden contener solo un subconjunto de claves (name y age se vuelven opcionales). Puedes comprobarlo así:
typescript
Zod también permite hacer opcionales solo ciertas propiedades pasando un objeto como parámetro (UserSchema.partial({ age: true }) solo hace opcional age). Ten en cuenta que .partial() solo actúa en el primer nivel; Zod también ofrece .deepPartial() para hacer opcionales recursivamente los campos anidados.
TypeScript: El Utility Type Required<T> es lo opuesto a Partial: crea un tipo a partir de T haciendo que todas sus propiedades sean obligatorias (no opcionales). Es útil para "solidificar" un tipo cuyas propiedades eran opcionales. Por ejemplo:
typescript
En este ejemplo, Required<Settings> es equivalente a { theme: string; fontSize: number } – la omisión de una propiedad se convierte en un error de compilación.
Equivalente con Zod: Para un esquema Zod, el equivalente es el método .required(), que transforma un esquema de objeto haciendo que todas las propiedades sean obligatorias (a diferencia de .partial() que las hace opcionales). Este método es especialmente útil si primero definiste un esquema parcial y luego quieres obtener la versión estricta. Por ejemplo:
typescript
Aquí StrictUserSchema requiere que name y age estén presentes. También puedes especificar solo ciertas propiedades: BaseUserSchema.required({ age: true }) solo hace obligatoria age (y deja name opcional).
Ten en cuenta que Required<T> actúa en tiempo de compilación (ya no existe en el código JavaScript emitido), mientras que schema.required() de Zod actúa en tiempo de ejecución validando la presencia de las claves durante el parseo.
TypeScript: El Utility Type Readonly<T> transforma un tipo T haciendo que sus propiedades sean de solo lectura (inmutables). Cualquier intento de reasignar estas propiedades será reportado como error en compilación. Por ejemplo:
typescript
Aquí, todo es de tipo Readonly<Todo> por lo que sus campos no pueden modificarse después de la inicialización. Esto suele corresponder al uso de Object.freeze en JavaScript, pero TypeScript solo lo gestiona a nivel de tipo (no impide realmente la mutación en tiempo de ejecución, solo avisa en compilación).
Equivalente con Zod: Zod ofrece una funcionalidad interesante para la inmutabilidad. El método .readonly() aplicado a un esquema devuelve un nuevo esquema que, al parsear, llama a Object.freeze() sobre el resultado, y cuyo tipo estático se marca como Readonly<...>. Es decir, Zod puede congelar el objeto validado y asegurar que corresponde a un tipo readonly. Por ejemplo:
typescript
Aquí, FrozenUserSchema congela el objeto devuelto por parse. Si intentas modificar frozenUser, se lanzará una excepción en tiempo de ejecución porque el objeto es inmutable. Así, Zod va más allá que TypeScript: donde Readonly<T> es puramente estático (las propiedades podrían modificarse en JS si se elude el tipado), el esquema .readonly() de Zod asegura inmutabilidad real en tiempo de ejecución además de proporcionar el tipo Readonly<...> correspondiente.
TypeScript: Pick<T, Keys> construye un nuevo tipo a partir de T manteniendo solo ciertas propiedades especificadas por Keys (típicamente una unión de literales de cadena que representan nombres de propiedades). Es útil para crear un tipo más restringido a partir de uno existente. Por ejemplo:
typescript
Aquí, TodoPreview solo mantiene title y completed de FullTodo. Cualquier propiedad no listada (por ejemplo description) se excluye del tipo resultante.
Equivalente con Zod: Los esquemas de objeto de Zod también tienen un método .pick() que crea un nuevo esquema que contiene solo las claves indicadas, de forma similar:
typescript
Pasas un objeto con las claves a mantener puestas en true. TodoPreviewSchema solo validará objetos con title y completed (las otras propiedades de FullTodo serán ignoradas por el parser por defecto). Por ejemplo, TodoPreviewSchema.parse({ title: "Clean", completed: false, description: "..." }) devolverá { title: "...", completed: false } habiendo eliminado description (porque por defecto Zod ignora las claves no especificadas, comportamiento que puede ajustarse con .strict() o .passthrough() si es necesario).
De forma similar, Zod proporciona .omit() para el efecto opuesto.
TypeScript: Omit<T, Keys> crea un nuevo tipo a partir de T eliminando ciertas propiedades (Keys). Es el complemento de Pick (de hecho Omit<T, K> suele implementarse como Pick<T, Exclude<keyof T, K>>). Ejemplo:
typescript
Aquí TaskMeta solo contiene id y title. Las propiedades done y dueDate han sido excluidas.
Equivalente con Zod: El método .omit() de Zod produce un esquema de objeto sin las claves indicadas. Ejemplo:
typescript
TaskMetaSchema validará objetos que solo contengan id y title. Cualquier clave omitida (done, dueDate) será ignorada si está presente en la entrada. Pick y Omit te permiten proyectar un esquema Zod sobre un subconjunto de campos igual que sus equivalentes de TypeScript a nivel de tipo.
TypeScript: Record<Keys, Type> construye un tipo de objeto cuyas claves son de tipo Keys (típicamente una unión de literales de cadena o un tipo string/number más general) y los valores son de tipo Type. Esto te permite describir, por ejemplo, un objeto indexado por identificadores. Un ejemplo clásico:
typescript
Aquí, Record<CatName, CatInfo> asegura que el objeto cats tenga exactamente las claves "miffy", "boris", "mordred", cada una asociada a un CatInfo. Record es muy útil para tipar objetos usados como diccionarios o mapas.
Equivalente con Zod: Zod ofrece el esquema z.record(keySchema, valueSchema) para validar objetos de este tipo. Puedes usarlo con un esquema de clave (por ejemplo z.string() o un esquema literal) y un esquema de valor. Ejemplo:
typescript
Aquí, AgeMapSchema valida cualquier clave siempre que sea una cadena (por defecto, z.record sin primer argumento asume string). Si tienes un conjunto finito de claves, a menudo es mejor usar z.object({ ... }) con claves explícitas o z.enum([...]) para limitar las claves. De hecho, Zod permite un esquema de clave más estricto: podrías definir const CatNameSchema = z.enum(["miffy","boris","mordred"]) y luego hacer z.record(CatNameSchema, CatInfoSchema) – sin embargo, en tiempo de ejecución las claves de los objetos JavaScript siempre son cadenas. Incluso una clave numérica se convertirá en cadena en tiempo de ejecución (por ejemplo, una clave 1 se convierte en "1" en un objeto JS). Zod tiene esto en cuenta y no permite definir un esquema de clave puramente numérico sin transformarlo en cadena.
En resumen, Record<Keys, Type> (TypeScript) y z.record(keySchema, valueSchema) (Zod) te permiten tipar diccionarios. El primero actúa a nivel de sistema de tipos, el segundo en tiempo de ejecución para validar la estructura del objeto.
TypeScript: Exclude<U, E> crea un tipo eliminando de la unión U todos los miembros asignables al tipo E. Es decir, excluyes ciertos tipos de una unión. Por ejemplo:
typescript
Aquí, Exclude<Role, "admin"> elimina el literal "admin" de la unión, dejando solo "user" | "guest". Otro ejemplo: Exclude<string | number | boolean, string> daría number | boolean (eliminando todo lo asignable a string). Es útil para refinar una unión eliminando casos no deseados.
Equivalente con Zod: Para restringir las posibilidades de una unión en tiempo de ejecución, normalmente defines un esquema que no incluya los casos a excluir. Por ejemplo, si tienes RoleSchema = z.enum(["admin","user","guest"]) puedes crear un subconjunto no incluyendo "admin". Zod proporciona para enumeraciones el método .exclude([...]) que genera un nuevo esquema enum sin los valores especificados. Por ejemplo:
typescript
Este NonAdminSchema corresponde exactamente al tipo Exclude<Role, "admin"> pero también asegura la validación en tiempo de ejecución. Internamente, RoleSchema.exclude(["admin"]) simplemente elimina "admin" de la lista de literales permitidos. De forma similar, Zod ofrece .extract([...]) para la operación complementaria.
TypeScript: Extract<T, U> crea un tipo manteniendo de T solo los miembros asignables a U. Es básicamente lo opuesto a Exclude. Por ejemplo:
typescript
Otro ejemplo: Extract<"a" | "b" | "c", "a" | "f"> dará "a" (porque solo "a" está en ambos). Este Utility Type se usa para filtrar una unión y extraer solo un subtipo concreto.
Equivalente con Zod: Como con Exclude, Zod permite obtener en tiempo de ejecución un esquema correspondiente a un subconjunto de una enumeración mediante .extract([...]). Tomemos el ejemplo de roles: para extraer solo, por ejemplo, los roles limitados "user" y "guest" de un RoleSchema completo:
typescript
Aquí LimitedRolesSchema corresponde al tipo Extract<Role, "user" | "guest">. En la práctica, podrías obtener el mismo resultado definiendo directamente z.enum(["user","guest"]), pero .extract es útil para derivar un esquema de un enum existente. Ten en cuenta que .exclude y .extract de Zod se aplican a esquemas enum (z.enum o z.nativeEnum), y no a uniones arbitrarias de tipos complejos. Para estos últimos, normalmente construyes manualmente el nuevo esquema de unión deseado.
TypeScript: NonNullable<T> construye un tipo excluyendo null y undefined de T. Es común en TypeScript tener tipos unión que incluyen estos valores "nullish" y querer un tipo que los excluya. Por ejemplo:
typescript
Aquí, DefinitelyString solo contiene string (ni null ni undefined). Este Utility Type se usa a menudo junto con condiciones de presencia (if (value != null) { ... }) para ayudar al compilador a refinar los tipos.
Equivalente con Zod: Con Zod, por defecto los esquemas no aceptan undefined o null a menos que lo especifiques. Por ejemplo, un esquema z.string() solo acepta una cadena, no undefined. Para permitir estos valores, usas explícitamente z.string().optional() (que permite undefined) o z.string().nullable() (que permite null) o incluso .nullish() (que permite ambos). Así, obtener el equivalente de un tipo NonNullable significa simplemente no incluir null/undefined en el esquema. Sin embargo, si partes de un esquema más permisivo y quieres hacerlo más estricto, puedes usar una combinación de técnicas: por ejemplo, el método .unwrap() permite extraer el esquema subyacente de un esquema opcional o nullable.
typescript
Aquí, OptionalName.unwrap() devuelve el esquema z.string() original. Esto te permite "eliminar" el carácter opcional o nullable introducido previamente. Otro enfoque sería usar una validación personalizada para rechazar null/undefined, pero sería redundante dado el comportamiento de los esquemas Zod.
TypeScript: Parameters<T> extrae los tipos de los parámetros de un tipo de función T como una tupla. Es útil para trabajar con firmas de funciones, especialmente cuando quieres crear una función que envuelve a otra. Por ejemplo:
typescript
Aquí, Parameters<typeof greet> extrae la tupla [string, number] que representa los parámetros de greet. Esto permite que safeGreet acepte exactamente los mismos parámetros que greet.
Equivalente con Zod: Zod proporciona z.function() para definir esquemas de función, con métodos para especificar los tipos de parámetros y el tipo de retorno. Aunque no hay un equivalente directo de Parameters<T>, puedes definir un esquema de función y extraer los tipos de parámetros usando Parameters de TypeScript sobre el tipo inferido:
typescript
Aquí, GreetSchema define un esquema de función que acepta una cadena y un número y devuelve una cadena. El método .implement() crea una función envoltorio que valida entradas y salidas según el esquema. Podemos extraer los tipos de parámetros usando Parameters<GreetFn>. Este enfoque combina validación en tiempo de ejecución con tipado estático.
TypeScript: ReturnType<T> extrae el tipo de retorno de un tipo de función T. Es útil cuando quieres trabajar con el resultado de una función sin tener que redefinir explícitamente el tipo. Por ejemplo:
typescript
Aquí, ReturnType<typeof createUser> extrae el tipo de objeto devuelto por createUser. Esto nos permite definir el tipo User sin duplicar la estructura.
Equivalente con Zod: Como con Parameters, no hay un equivalente directo en Zod para ReturnType, pero puedes combinar el esquema de función de Zod con ReturnType de TypeScript:
typescript
Aquí, definimos un esquema de función con CreateUserSchema, luego extraemos el tipo de retorno de dos maneras: usando ReturnType de TypeScript sobre el tipo de función inferido, o usando el método .returnType() de Zod que devuelve el esquema para el valor de retorno. Ambos enfoques nos dan el tipo User.
TypeScript: InstanceType<T> extrae el tipo de instancia de un tipo constructor T. Es útil cuando tienes una referencia a una clase y quieres trabajar con instancias de esa clase. Por ejemplo:
typescript
Aquí, InstanceType<typeof Point> nos da el tipo de una instancia de Point, que incluye sus propiedades y métodos.
Equivalente con Zod: Zod no tiene un equivalente directo para tipos de instancia de clase, ya que se centra en la validación de datos más que en el comportamiento de clases. Sin embargo, puedes definir un esquema para la forma de las instancias de clase:
typescript
Este enfoque define un esquema que corresponde a la forma de una instancia de Point, pero no captura la relación completa de la clase. Para una validación de clase más compleja, podrías combinar Zod con class-validator u otras librerías.
Los Utility Types de TypeScript y los esquemas de Zod sirven ambos para garantizar la fiabilidad de los datos, pero a diferentes niveles: uno en tiempo de compilación, el otro en tiempo de ejecución.
En resumen:
Filosóficamente, los utility types expresan transformaciones o restricciones a nivel de sistema de tipos (ayudan al desarrollador, sin impacto directo en el código emitido), mientras que Zod proporciona contratos a nivel de ejecución (permiten la implementación concreta de validaciones y transformaciones de datos en ejecución). Ambos enfoques responden a necesidades diferentes pero relacionadas: mantener la robustez del código. Además, Zod está diseñado para integrarse armoniosamente con TypeScript: infiere automáticamente los tipos estáticos de tus esquemas, evitando la duplicación de definiciones. Esto significa que a menudo puedes definir un esquema Zod (para validación) y usar z.infer para derivar el tipo estático correspondiente de TypeScript, en lugar de definir una interfaz TypeScript y un esquema separado – teniendo así una sola fuente de verdad para tus tipos de datos.
En conclusión, conocer los Utility Types de TypeScript te permite aprovechar el sistema de tipos para escribir código seguro y expresivo, mientras que dominar Zod te da las herramientas para hacer cumplir estos contratos de tipo en tiempo de ejecución. Usados juntos, refuerzan enormemente la fiabilidad de tus aplicaciones TypeScript: TypeScript te protege de errores de programación, Zod protege tu aplicación de datos inesperados del exterior. ¡Crea tus tipos, valida tus datos y programa con tranquilidad 🎉!