Cuarto post de la serie y aquí es donde TypeScript empieza a ponerse muy interesante. Si vienes siguiendo desde el principio (post 1, post 2, post 3), ya sabes tipar variables, funciones y callbacks y ahora toca darle forma a tus datos.
Hoy vamos a crear contratos contratos En TypeScript, un contrato es la definición explícita de la forma que debe tener un dato. Si el dato no cumple el contrato, el compilador lo rechaza. para tus objetos, esto quiere decir que vamos a definir exactamente qué propiedades tienen, cuáles son opcionales, cuáles son de solo lectura, y acabaremos con algo super útil para ahorrar código con el que vamos a alucinar pepinillos que son los Template Union Types. EA, Vamos al lío.

1. El problema de los objetos sin forma
En nuestro querido caotico JavaScript un objeto puede ser lo que quiera. Hoy tiene nombre, mañana tiene age, pasado le metes un toString que devuelve un array. Nadie te dice nada de las propiedades que tiene, tu no sabes que va a contener ese objeto, es un kinder sorpresa.
// JavaScript: anarquía total
const heroe = {
nombre: 'Thor',
edad: 1500,
};
heroe.nombre = 42; // JS: "vale, tú mandas"
heroe.poder = 'rayo'; // JS: "claro, lo que quieras"
delete heroe.edad; // JS: "faltaría más"
En TypeScript, nada de esto pasa porque cuando creas un objeto, TypeScript infiere su forma y la convierte en un contrato implícito:
// TypeScript: la inferencia ya te protege
const heroe = {
nombre: 'Thor',
edad: 1500,
};
heroe.nombre = 42; // ❌ Error: Type 'number' is not assignable to type 'string'
heroe.poder = 'rayo'; // ❌ Error: Property 'poder' does not exist
heroe.edad = undefined; // ❌ Error: Type 'undefined' is not assignable to type 'number'
Pero la inferencia tiene un límite y funciona para un objeto concreto. ¿Y si quieres que varios objetos sigan la misma forma? ¿Si quieres reutilizar esa estructura en funciones, arrays, APIs? Ahí ya entran los Type Aliases.
2. Type Aliases para darle nombre a tus formas
Un 💡 Type Alias Una forma de darle un nombre reutilizable a un tipo en TypeScript. Se define con la palabra clave 'type' y se escribe en PascalCase. Puede representar objetos, uniones, intersecciones y más. Más info → es como ponerle nombre a un molde. Lo defines una vez y luego lo usas para crear tantos objetos como quieras, todos con la misma forma exacta.
Vamos a construir nuestro tipo Hero paso a paso:
Versión básica
// Definimos el tipo con 'type' y PascalCase
type Hero = {
nombre: string;
edad: number;
};
// Ahora creamos héroes que CUMPLEN el contrato
const thor: Hero = {
nombre: 'Thor',
edad: 1500,
};
const spiderman: Hero = {
nombre: 'Spider-Man',
edad: 17,
};
// ❌ Estos NO cumplen el contrato
const hulkMal: Hero = {
nombre: 'Hulk',
// Error: Property 'edad' is missing in type
};
const ironmanMal: Hero = {
nombre: 'Iron Man',
edad: 48,
empresa: 'Stark Industries',
// Error: Object literal may only specify known properties,
// and 'empresa' does not exist in type 'Hero'
};
TypeScript es muy estricto por lo que si faltan propiedades lanzará un error, si sobran propiedades, error. El objeto tiene que ser exactamente lo que dice el tipo y ya está, nada más.
Convención: Los Type Aliases se escriben en PascalCase PascalCase Estilo de nombrado donde cada palabra empieza con mayúscula sin separadores: MiTipo, HeroData, UserProfile. Es la convención estándar para tipos e interfaces en TypeScript. :
Hero,UserProfile,ProductData. Nuncahero,user_profileniHERO. Es una convención universal que todo el ecosistema TypeScript sigue.
3. Propiedades opcionales con el modificador ?
No siempre necesitas que todas las propiedades estén presentes. Algunos héroes tienen poderes, otros no. Para eso existe el modificador ?:
type Hero = {
nombre: string;
edad: number;
poder?: string; // Opcional: puede estar o no
};
// ✅ los dos son válidos
const thor: Hero = {
nombre: 'Thor',
edad: 1500,
poder: 'Trueno',
};
const hawkeye: Hero = {
nombre: 'Hawkeye',
edad: 42,
// Sin 'poder': válido porque es opcional
};
Cuando una propiedad es opcional, su tipo real es string | undefined. Esto significa que antes de usarla tienes que comprobar:
function mostrarPoder(heroe: Hero): string {
// ❌ Error potencial: heroe.poder puede ser undefined
return heroe.poder.toUpperCase();
// ✅ Comprobación primero
if (heroe.poder) {
return heroe.poder.toUpperCase();
}
return 'Sin poderes especiales';
}
TypeScript te obliga a manejar el caso undefined. Nada de Cannot read property 'toUpperCase' of undefined en runtime. Ese error lo cazas antes de ejecutar. Pos ea, pa eso estamos.
4. Propiedades de solo lectura con el modificador readonly
Hay propiedades que nunca deberían cambiar después de crear el objeto. El id de un usuario, la fecha de creación de un post, el ADN de un héroe… Para eso está 💡 readonly Modificador de TypeScript que impide la reasignación de una propiedad después de la creación del objeto. Solo funciona en tiempo de compilación — en JavaScript puro, la propiedad sigue siendo mutable.
Más info →
:
type Hero = {
readonly id: string;
nombre: string;
edad: number;
poder?: string;
};
const thor: Hero = {
id: 'hero-001',
nombre: 'Thor',
edad: 1500,
poder: 'Trueno',
};
thor.nombre = 'Thor Odinson'; // ✅ Se puede cambiar
thor.edad = 1501; // ✅ Se puede cambiar
thor.id = 'hero-999'; // ❌ Error: Cannot assign to 'id'
// because it is a read-only property
readonly bloquea la reasignación en tiempo de compilación. Pero, ojito:
Avisa en el editor si intentas reasignar la propiedad. El compilador da error y no compila. Protege contra errores accidentales.
No hace el objeto inmutable en runtime. En el JavaScript compilado, la propiedad sigue siendo mutable. Es solo una protección en desarrollo.
Esto es consistente con cómo funciona TypeScript: todo desaparece al compilar a JavaScript. Los tipos, los readonly, los opcionales… todo se evapora porque son herramientas de desarrollo, no de runtime. Si necesitas inmutabilidad real en ejecución, usa Object.freeze().
Nuestro Hero completo hasta ahora
Mira cómo ha evolucionado nuestro tipo paso a paso:
// Versión 1: Básico
type HeroV1 = {
nombre: string;
edad: number;
};
// Versión 2: Con opcionales
type HeroV2 = {
nombre: string;
edad: number;
poder?: string;
};
// Versión 3: Con readonly (versión final)
type Hero = {
readonly id: string;
nombre: string;
edad: number;
poder?: string;
activo?: boolean;
};
5. Union Types en objetos
Ya vimos los tipos unión con primitivos (string | number). Pero con objetos se ponen mucho más interesantes porque puedes limitar los valores posibles de una propiedad:
type Hero = {
readonly id: string;
nombre: string;
edad: number;
poder?: string;
bando: 'heroe' | 'villano' | 'antihéroe'; // Solo estos 3 valores
};
const venom: Hero = {
id: 'hero-003',
nombre: 'Venom',
edad: 5,
bando: 'antihéroe', // ✅
};
const thanos: Hero = {
id: 'vil-001',
nombre: 'Thanos',
edad: 1000,
bando: 'jefe final',
// ❌ Error: Type '"jefe final"' is not assignable
// to type '"heroe" | "villano" | "antihéroe"'
};
Estos se llaman 💡 tipos literales Tipos que representan un valor exacto y concreto, no una categoría general. 'admin' es un tipo literal (solo puede ser ese string exacto), mientras que string puede ser cualquier cadena.
Más info →
. En vez de aceptar cualquier string, solo aceptan los valores que tú definas. Es autocompletado gratuito: al escribir bando: el editor te sugiere las tres opciones.
Extrayendo la unión a un type separado
Si la unión se reutiliza, mejor extraerla:
type Bando = 'heroe' | 'villano' | 'antihéroe';
type Poder = 'fuerza' | 'velocidad' | 'magia' | 'tecnología' | 'psíquico';
type Hero = {
readonly id: string;
nombre: string;
edad: number;
poder?: Poder;
bando: Bando;
};
// Ahora 'Bando' y 'Poder' son reutilizables en otras partes del código
function filtrarPorBando(heroes: Hero[], bando: Bando): Hero[] {
return heroes.filter((h) => h.bando === bando);
}
6. Template Union Types: la joya de la corona
Y aquí viene la parte del post que te va a dejar haciendo volteretas. Los 💡 Template Union Types Sistema de tipos de TypeScript que permite definir patrones en strings usando template literals. Puedes forzar que un string siga una estructura exacta, como 'abc-123-xyz' o '#ff0000'. Más info → te permiten forzar patrones en strings. No solo “esto es un string”, sino “esto es un string que tiene exactamente esta forma”.
Ejemplo 1 con IDs con formato
Imagina que tus IDs deben seguir el patrón xxx-xxx-xxx (tres bloques de texto separados por guiones):
// Definimos el patrón con template literal
type HeroId = `${string}-${string}-${string}`;
type Hero = {
readonly id: HeroId;
nombre: string;
};
// ✅ Cumplen el patrón
const thor: Hero = { id: 'hero-001-asgard', nombre: 'Thor' };
const spidey: Hero = { id: 'nyc-spdr-teen', nombre: 'Spider-Man' };
// ❌ No cumplen el patrón
const hulkMal: Hero = { id: 'hulk', nombre: 'Hulk' };
// Error: Type '"hulk"' is not assignable to type '`${string}-${string}-${string}`'
const thanosMal: Hero = { id: '12345', nombre: 'Thanos' };
// Error: no tiene la estructura xxx-xxx-xxx
TypeScript valida en compilación que el string sigue el patrón pero si alguien pone un ID sin los dos guiones, error al instante. En un proyecto real con miles de IDs circulando, esto te salva de bugs silenciosos y puñeteros.
Ejemplo 2 con Colores hexadecimales
Quieres que los colores siempre empiecen con #:
type HexColor = `#${string}`;
type Tema = {
primario: HexColor;
secundario: HexColor;
fondo: HexColor;
};
const miTema: Tema = {
primario: '#4ade80', // ✅
secundario: '#60a5fa', // ✅
fondo: 'ffffff', // ❌ Error: Type '"ffffff"' is not assignable
// to type '`#${string}`'
};
Se acabó el me olvidé del # que luego rompe todo el CSS en producción porque nuestro friend TypeScript te lo caza en el editor.
Ejemplo 3 con Combinaciones con Union Types
Aquí es donde la cosa se pone topedeloca porque puedes combinar Template Literals con Union Types para generar todas las combinaciones posibles:
type Tamaño = 'sm' | 'md' | 'lg' | 'xl';
type Orientacion = 'horizontal' | 'vertical';
// TypeScript genera TODAS las combinaciones automáticamente
type ClaseLayout = `layout-${Tamaño}-${Orientacion}`;
// Resultado: 'layout-sm-horizontal' | 'layout-sm-vertical'
// | 'layout-md-horizontal' | 'layout-md-vertical'
// | 'layout-lg-horizontal' | 'layout-lg-vertical'
// | 'layout-xl-horizontal' | 'layout-xl-vertical'
function aplicarLayout(clase: ClaseLayout) {
document.body.className = clase;
}
aplicarLayout('layout-md-horizontal'); // ✅
aplicarLayout('layout-xxl-diagonal'); // ❌ Error: no existe esa combinación
De 2 tipos con 4 y 2 valores respectivamente, TypeScript genera 8 combinaciones válidas automáticamente. Y el autocompletado te las sugiere todas, esto es magia de tipos, nene.
Ejemplo 4 con Eventos tipados
Un caso real que te vas a encontrar en proyectos:
type Entidad = 'usuario' | 'producto' | 'pedido';
type Accion = 'crear' | 'actualizar' | 'eliminar';
type EventoApp = `${Entidad}:${Accion}`;
// 'usuario:crear' | 'usuario:actualizar' | 'usuario:eliminar'
// | 'producto:crear' | 'producto:actualizar' | 'producto:eliminar'
// | 'pedido:crear' | 'pedido:actualizar' | 'pedido:eliminar'
function emitirEvento(evento: EventoApp, data: unknown): void {
console.log(`[EVENT] ${evento}`);
}
emitirEvento('usuario:crear', { nombre: 'Domin' }); // ✅
emitirEvento('usuario:login', {}); // ❌ Error: 'login' no es una acción válida
emitirEvento('admin:crear', {}); // ❌ Error: 'admin' no es una entidad válida
9 combinaciones válidas, todas con autocompletado, sin escribirlas a mano. Y si mañana añades una nueva entidad o acción, las combinaciones se regeneran solas.
7. Anidando tipos: objetos dentro de objetos
Los tipos se pueden anidar para estructuras más complejas:
type Direccion = {
calle: string;
ciudad: string;
codigoPostal: string;
pais: string;
};
type RedSocial = {
plataforma: 'twitter' | 'github' | 'linkedin';
url: string;
};
type Hero = {
readonly id: `${string}-${string}-${string}`;
nombre: string;
edad: number;
poder?: 'fuerza' | 'velocidad' | 'magia' | 'tecnología';
bando: 'heroe' | 'villano' | 'antihéroe';
base: Direccion;
redesSociales: RedSocial[];
};
const ironman: Hero = {
id: 'avng-iron-001',
nombre: 'Tony Stark',
edad: 48,
poder: 'tecnología',
bando: 'heroe',
base: {
calle: '10880 Malibu Point',
ciudad: 'Malibu',
codigoPostal: '90265',
pais: 'USA',
},
redesSociales: [
{ plataforma: 'twitter', url: 'https://twitter.com/tonystark' },
{ plataforma: 'github', url: 'https://github.com/ironman' },
],
};
Cada nivel del objeto tiene autocompletado completo. Escribes ironman.base. y te sugiere calle, ciudad, codigoPostal, pais. Escribes ironman.redesSociales[0].plataforma y solo acepta 'twitter' | 'github' | 'linkedin', la vida es bella.
8. Errores comunes con objetos y types
type heroData = ... — Mal. Los tipos van en PascalCase: type HeroData = .... Es convención universal.
readonly solo protege en compilación. En el JS generado, la propiedad sigue siendo mutable. Usa Object.freeze() si necesitas inmutabilidad real.
Si usas 'admin' | 'user' | 'guest' en 5 sitios, crea un type Rol = ... y reutilízalo. DRY aplica a los tipos también.
Una propiedad poder? es string | undefined. Si la usas sin comprobar, TypeScript te dejará... pero el runtime no.
9. Checklist: domina objetos y Type Aliases
Conclusión
Hoy hemos subido de nivel para hacer un código mucho más estricto y tipado. Ya no solo tipamos variables y funciones, sino que ahora definimos la forma exacta de los datos. Con los Type Aliases creas contratos que todo tu código debe respetar y con ? y readonly controlas qué es obligatorio y qué es intocable, además, con los Template Union Types puedes forzar patrones en strings que en JavaScript serían imposibles de validar sin regex y tests manuales.
La combinación de Union Types con Template Literals es una de las funcionalidades más potentes de TypeScript. Úsala para IDs, eventos, clases CSS, rutas de API… las posibilidades son infinitas.
En el próximo post veremos la batalla definitiva: type vs interface, cuándo usar cada uno, intersecciones con &, y cómo extender tipos. Esa es la pregunta que todo el mundo hace en las entrevistas.
EA, nos vemos en los bares 🍻
Pon a prueba lo aprendido
1. ¿Con qué convención de nombrado se escriben los Type Aliases?
2. ¿Qué tipo real tiene una propiedad marcada con ? (opcional)?
3. ¿Qué protección ofrece readonly en tiempo de ejecución?
4. ¿Qué es un Template Union Type?
5. Si defines type Color = `#${string}`, ¿cuál de estos valores es válido?
6. ¿Cuántas combinaciones genera type Clase = `btn-${'sm' | 'md' | 'lg'}-${'primary' | 'danger'}`?