En un proyecto pequeño es fácil pensar que TypeScript solo añade anotaciones a cosas que ya funcionan. En este portfolio empezó a cobrar sentido cuando el contenido, las rutas y los metadatos dejaron de caber en una sola página. Un título no es solo texto. Puede acabar en una tarjeta del blog, en el <title>, en datos estructurados, en el sitemap y en dos idiomas.
No uso TypeScript para hacer que el proyecto parezca más complejo. Lo uso para que algunas incoherencias aparezcan durante el desarrollo o el build, cuando todavía puedo corregirlas sin recorrer todas las rutas publicadas.
Los tipos no sustituyen una revisión. Sirven para acordar qué datos necesita cada parte de la web y para señalar cuándo el código deja de respetar ese acuerdo.
El contenido deja de ser un bloque de texto
Los artículos y proyectos se escriben en MDX, pero antes de renderizarse tienen una forma definida en src/content.config.ts. Para los artículos, el esquema exige título, resumen y fecha de publicación. También define los campos que el sistema usa para etiquetas, versiones en inglés, SEO, imágenes sociales y tiempo de lectura.
const posts = defineCollection({
loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/blog' }),
schema: z.object({
title: z.string(),
title_en: z.string().optional(),
seoTitle: z.string().optional(),
seoTitle_en: z.string().optional(),
summary: z.string(),
summary_en: z.string().optional(),
ogImage: z.string().optional(),
ogImage_en: z.string().optional(),
tags: z.array(z.string()).default([]),
tags_en: z.array(z.string()).optional(),
publishedAt: z.coerce.date(),
updatedAt: z.coerce.date().optional(),
draft: z.boolean().default(false),
readingTime: z.number().optional(),
}),
})La parte importante no es que ese archivo termine en .ts. Cada entrada pasa por el mismo contrato. Si olvido publishedAt, el problema no se descubre cuando alguien ya ha visitado una URL sin fecha. El build rechaza la entrada. Si escribo una fecha imposible, el fallo aparece en el contenido que hay que corregir.
Después, ese mismo dato alimenta la cabecera visible del artículo, el objeto BlogPosting y el lastmod del sitemap. No tengo que recordar tres veces que una pieza se actualizó. El dato existe una vez y las capas que lo usan lo reciben desde la colección.
La documentación de Content Collections de Astro describe esta validación como parte del sistema de contenido. En este proyecto me protege más de una entrada incompleta que de un error de sintaxis llamativo.
Un campo opcional también es una decisión
El esquema permite que title_en y summary_en sean opcionales. Eso da margen a entradas antiguas y hace posible usar el español como respaldo cuando una traducción aún no existe. Evita que una ruta se rompa por un dato ausente.
También deja ver un límite. Marcar una propiedad como opcional significa que el código debe admitir que falte. No puedo decir que una web es bilingüe por añadir title_en?: string. Hay que escribir ambos textos, revisar sus enlaces y abrir la página inglesa publicada.
No siempre el tipo correcto es el más estricto. A veces una propiedad opcional está escondiendo un requisito que debería ser obligatorio antes de publicar. Depende de quién edita, qué flujo de revisión existe y qué versión puede llegar a producción.
En cómo organizo contenido bilingüe con Astro y MDX explico la convención que uso aquí. TypeScript ayuda con los campos del frontmatter, pero no puede decidir si una traducción conserva la intención del original.
Las props de un componente son una frontera
Los tipos también ganan valor cuando un componente se reutiliza. Una tarjeta de proyecto no recibe un objeto parecido a un proyecto. Recibe una entrada real de la colección projects.
import type { CollectionEntry } from 'astro:content'
interface Props {
project: CollectionEntry<'projects'>
featured?: boolean
variant?: 'grid' | 'archive'
index?: number
}Con ese contrato, ProjectCard sabe que title, stack y year respetan el esquema de proyectos y que thumbnail, si existe, será una cadena. Quien lo usa no puede pasar por accidente un artículo del blog ni un objeto improvisado con solo título e imagen.
También restringe las variantes a grid y archive. Parece una diferencia menor hasta que el componente aparece en varios listados. Sin ese límite, alguien puede escribir variant="featured", la página seguirá teniendo HTML y el componente dejará de representar lo que esperaba el diseño.
El tipo no convierte el componente en una caja cerrada. Si aparece una variante nueva, hay que diseñarla, añadirla al contrato y revisar responsive, accesibilidad y contenido. El tipo impide nombres inventados, no toma esa decisión por mí.
Astro recomienda declarar una interfaz Props en el frontmatter de los componentes. Es un patrón sencillo, pero evita que la interfaz de un componente quede escondida entre condiciones y valores por defecto. Está explicado en su guía de TypeScript.
Los metadatos tienen demasiados destinos para depender de la memoria
Una página necesita título, descripción, imagen, canonical, alternates de idioma y, cuando corresponde, fechas para redes sociales y datos estructurados. Copiar esos valores en cada plantilla sería una forma rápida de que una URL en inglés apunte a una imagen española o que una descripción cambie en un sitio y no en otro.
Por eso Seo.astro define qué puede recibir:
interface Props {
title?: string
description?: string
image?: string
type?: 'website' | 'article'
publishedTime?: string
updatedTime?: string
noindex?: boolean
siteName?: string
}Una plantilla no puede pasar type="post" esperando que sea lo mismo que article. El editor avisa antes. Tampoco puede confundir una fecha con un booleano o una imagen con un objeto. Son errores poco llamativos, pero se multiplican cuando existen artículos, proyectos, hubs, páginas de servicio y dos idiomas.
En las páginas de artículo, los datos de la colección construyen el BlogPosting, y la ruta construye la canonical desde el slug. Esa separación permite mantener una regla clara: el contenido define de qué trata la página y la plantilla define cómo se publica y se describe fuera de ella.
Es parte de la arquitectura SEO de este portfolio. TypeScript reduce incoherencias dentro del proyecto. No garantiza que una descripción responda a una búsqueda ni que Google elija la canonical declarada.
Los tipos acompañan las rutas, pero no sustituyen el mapa de idiomas
El proyecto mantiene los locales es y en en los puntos donde hace falta. Los helpers de idioma resuelven después las rutas y el <html lang>. Una tarjeta calcula su enlace desde el locale actual y el identificador de la colección, en lugar de recibir una URL escrita a mano cada vez.
Eso elimina una clase concreta de errores y deja más claro qué parte del sistema es responsable de cada cosa. No significa que la internacionalización esté resuelta por tener un union type. Aquí, español e inglés comparten un MDX y el build retira el bloque que no corresponde a cada página final. Esa convención necesita pruebas sobre el HTML generado.
Los tipos no saben si el texto inglés dice lo que debía decir ni si un ejemplo funciona igual para otra audiencia. La utilidad está en repartir responsabilidades. El tipo limita valores posibles, el helper construye la ruta, la plantilla publica hreflang y canonical, y el build comprueba el resultado.
La comprobación importante ocurre antes de desplegar
Un proyecto puede tener tipos y seguir fallando al construir. Por eso no me quedo en el autocompletado del editor. pnpm check ejecuta astro check para revisar archivos .astro y .ts. Después, pnpm build genera las rutas estáticas. Hay pruebas que inspeccionan el HTML de dist para comprobar schema, canonicals, hreflang, enlaces internos y páginas de servicio.
El orden que sigo es este:
- El esquema rechaza datos de contenido incompletos.
- Las props tipadas detectan usos incompatibles entre componentes.
astro checkencuentra problemas entre Astro y TypeScript.- El build demuestra que se pueden generar las rutas.
- Las pruebas del resultado publicado comprueban cosas que los tipos no ven.
No es una cadena infalible. Un enlace externo puede caer después, una regla del hosting puede cambiar una cabecera o una persona puede encontrar un texto confuso. Pero llegar a producción con esas capas revisadas es distinto de confiar en que una página “parece funcionar” en local.
Lo que TypeScript no arregla por mí
Hay una tentación bastante común de usar el tipado como sinónimo de calidad. No lo es.
TypeScript no comprueba que un presupuesto sea razonable, que un texto responda una pregunta útil ni que un alt genérico sea accesible. No protege los datos de un formulario en el servidor. Tampoco valida la respuesta de una API si no la valido al entrar al sistema.
No evita decisiones discutibles en este portfolio. El esquema actual permite campos opcionales y una única entrada MDX bilingüe. Reducen fricción aquí, pero podrían ser malas elecciones para un equipo editorial que necesita borradores, revisión por idioma o publicación independiente.
El valor de TypeScript no está en prometer que no habrá errores. Hace explícitas las suposiciones que ya existen. Cuando una cambia, el editor, la comprobación o el build tienen más posibilidades de señalar los lugares que debo revisar.
Por eso lo uso con Astro. No hace una web profesional por sí solo. Me ayuda a mantener alineados contenido, componentes y metadatos mientras el proyecto deja de ser una página y empieza a tener consecuencias en más de una ruta.
Puedes ver esas decisiones aplicadas a páginas, proyectos y contenido bilingüe en mis proyectos o en la reconstrucción de este portfolio con Astro.