Cómo adaptar Astro a TypeScript


Astro usa TypeScript por defecto así que, para crear un blog (o cualquier otro tipo de web) con Astro, puedes seguir los pasos que te conté en “¿Cómo crear un blog con Astro?” teniendo en cuenta los cambios que te comento a continuación:

Paso 1: Crear el proyecto con Astro

Crea un nuevo proyecto Astro utilizando yarn:

yarn create astro

Durante el proceso de configuración, responde a las siguientes preguntas (y fíjate en el cambio en la pregunta de TypeScript):

Opciones recomendadas para configurar el proyecto Astro y TypeScript con yarn

  1. ¿Dónde quieres crear tu nuevo proyecto?
    • ./astro-blog-template (o como quiera que se llame tu proyecto)
  2. ¿Cómo te gustaría empezar tu nuevo proyecto?
    • Usar la plantilla para blog
  3. ¿Planeas escribir en TypeScript?
  4. ¿Qué tan estricto debe ser TypeScript?
    • Estricto (Recomendado)
  5. ¿Instalar dependencias?
  6. ¿Inicializar un nuevo repositorio git?

Nota 1:

Yo he puesto astro-blog-template como nombre de proyecto porque he escrito este artículo a la vez que he creado la plantilla. Sustituye ese nombre por el que más te interese.

Además, al final del artículo te digo cómo usar la plantilla y que tengas de serie todo esto (y algunos extras que explico en otros artículos).

Nota 2:

Puedes responder de otras formas (obviamente) pero yo te recomiendo que uses las mismas que yo.

Paso 2: Configurar el repositorio

Igual que en el artículo “¿Cómo crear un blog con Astro? - Paso 2: Configurar el repositorio”.

Paso 3: Comprueba que todo funciona

Puedes hacerlo abriendo una terminal en la carpeta del proyecto y ejecutando:

yarn dev

y accediendo a http://localhost:4321/.

Nota:

Mira la web tras cada paso que completes a partir de ahora para comprobar que todo sigue igual (es decir, que no has roto la web).

yarn dev se encarga de revisar si hay cambios y actualiza la web con ellos de manera automática.

Paso 4: Mejorar la configuración del proyecto

Igual que en el artículo “¿Cómo crear un blog con Astro? - Paso 4: Mejorar la configuración del proyectopero teniendo en cuenta que la configuración en tsconfig.json extiende astro/tsconfigs/strict en vez de astro/tsconfigs/base, por lo que tu archivo de configuración (tras los cambios) debería verse así:

{
	"extends": "astro/tsconfigs/strict",
	"compilerOptions": {
		"baseUrl": "src/",
		"paths": {
		"@/*": ["./*"],
			"@components/*": ["components/*"],
			"@layouts/*": ["layouts/*"],
			"@styles/*": ["styles/*"]
		},
		"strictNullChecks": true
	}
}

Paso 5: Aplica tus datos sobre el proyecto

Igual que en el artículo “¿Cómo crear un blog con Astro? - Paso 5: Personalizar la estructura del proyecto”.

Paso 6: Corregir los fallos de TypeScript

Para muchos, es un quebradero de cabeza. Para mí, un pequeño incordio que luego me ahorra problemas y tests.

Crea src/types/collections.d.ts

Soy poco amigo de hacer cambios completos pero, en este caso, vamos a usar los tipos en varios sitios así que vamos a cubrirnos a futuro.

Al lío:

  1. Crea el archivo src/types/collections.d.ts (implica crear la carpeta src/types/)
  2. Escribe lo siguiente en el archivo src/types/collections.d.ts:
    import { type CollectionEntry } from 'astro:content';
    
    export type Collection = "blog";
    export type Post = CollectionEntry<'blog'>;
    

Nota:

Por convención, los archivos de TypeScript que contienen (solo) definiciones de tipos tienen el sufijo d (definition), por eso el archivo que acabas de crear se llama collections.d.ts para indicar que contiene las definiciones de tipos de colección (de contenidos de Astro).

¿Por qué no modificamos tsconfig.json?

¿Recuerdas las rutas que hemos creado en el Paso 4? Seguramente sentirás la tentación de añadir una nueva para @types.

Seguramente, cambiarás tu archivo tsconfig.ts para verse así:

{
    "extends": "astro/tsconfigs/strict",
    "compilerOptions": {
        "baseUrl": "src/",
        "paths": {
            "@/*": ["./*"],
            "@components/*": ["components/*"],
            "@layouts/*": ["layouts/*"],
            "@styles/*": ["styles/*"],
            "@types/*": ["types/*"]
        },
        "strictNullChecks": true
    }
}

Y seguramente verás el fallo de importación.

¿Qué ha pasado?

@types es un prefijo de paquetes de tipo.

Por ejemplo, si vas a usar ReactJS con TypeScript, instalarás @types/react como dependencia.

Por eso, si creas la ruta en tsconfig.ts, el proyecto no puede decidir si es un paquete externo (en node_modules) o si es una definición interna al proyecto.

¿Cómo lo solucionamos?

No necesitas hacer nada.

Si miras tu configuración, verás que creaste la siguiente línea:

"@/*": ["./*"]

Con eso, estás haciendo que @/ apunte a src/ así que puedes hacer la importación de tipos (internos) con @/types/.

Modifica src/pages/blog/[...slug].astro

Inicialmente, el archivo se ve así:

---
import { type CollectionEntry, getCollection } from 'astro:content';
import BlogPost from '@layouts/BlogPost.astro';

export async function getStaticPaths() {
const posts = await getCollection('blog');
    return posts.map((post) => ({
        params: { slug: post.slug },
        props: post,
    }));
}
type Props = CollectionEntry<'blog'>;

const post = Astro.props;
const { Content } = await post.render();
---

<BlogPost {...post.data}>
    <Content />
</BlogPost>

Y verás que, en la línea 6, post aparece marcado con el fallo:

Parameter 'post' implicitly has an 'any' type.ts(7006).

Y es que a TypeScript estricto (lo que hemos configurado para nuestro proyecto) “no es fan” del tipo any ya que permite cualquier tipo y, por lo tanto, es suceptible de fallo.

Por fortuna, ya hemos creado los tipos necesarios en el archivo src/types/collections.d.ts.

Solo queda importar los tipos y sustituir donde sea necesario.

Tras los cambios, el archivo debería verse como sigue:

---
import { getCollection } from 'astro:content';

import BlogPost from '@layouts/BlogPost.astro';
import type { Collection, Post } from '@/types/collections.d';

export async function getStaticPaths() {
    const collection: Collection = "blog";
    const posts: Post[] = await getCollection(collection);
        return posts.map((post) => ({
            params: { slug: post.slug },
            props: post,
        }));
}

type Props = Post;

const post = Astro.props;
const { Content } = await post.render();
---

<BlogPost {...post.data}>
    <Content />
</BlogPost>

Con esto, desaparecen los fallos en este archivo.

Nota:

En algunos frameworks basados en JavaScript (como Astro o como NextJS), nos encontramos nombres de carpetas o ficheros entre corchetes.

El caso más cercano ahora mismo es este mismo archivo: src/pages/blog/[...slug].astro.

Esto implica que, al procesar una URL del tipo blog/resto-de/la-url, Astro cogerá todo lo que vaya detrás de la parte que conoce (en este caso /blog) y lo guardará en un parámetro llamado slug para poder acceder a dichos datos desde el propio archivo.

Puedes descubrir más detalles y conseguir una mejor comprensión en la documentación oficial de Astro.

Modifica src/pages/blog/index.astro

En el archivo src/pages/blog/index.astro que nos servirá de página principal del blog (o página de archivo del blog), nos encontramos un par de fallos de TypeScript.

A continuación te dejo cómo se ve el archivo actualmente (por conveniencia he quitado las líneas de CSS) en el que deberías ver fallos marcados en las líneas 10 y 94:

---
import BaseHead from '@components/BaseHead.astro';
import Header from '@components/Header.astro';
import Footer from '@components/Footer.astro';
import { SITE_TITLE, SITE_DESCRIPTION } from '@/consts';
import { getCollection } from 'astro:content';
import FormattedDate from '@components/FormattedDate.astro';

const posts = (await getCollection('blog')).sort(
    (a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
);
---
<!doctype html>
<html lang="en">
    <head>
        <BaseHead title={SITE_TITLE} description={SITE_DESCRIPTION} />
        <style>
        // líneas 19 a 85
        </style>
    </head>
    <body>
        <Header />
        <main>
            <section>
                <ul>
                    {
                        posts.map((post) => (
                            <li>
                                <a href={`/blog/${post.slug}/`}>
                                    <img width={720} height={360} src={post.data.heroImage} alt="" />
									<h4 class="title">{post.data.title}</h4>
									<p class="date">
									    <FormattedDate date={post.data.pubDate} />
									</p>
								</a>
							</li>
						))
					}
				</ul>
			</section>
		</main>
		<Footer />
    </body>
</html>

Digo fallos… en realidad es el mismo fallo: Parameter '...' implicitly has an 'any' type.ts(7006).

Al igual que con el caso anterior (el archivo src/pages/blog/[...slug].astro), tenemos preparada la solución con los tipos que hemos definido en el archivo src/types/collections.d.ts.

Tras los cambios, el archivo debería quedarte como puedes ver a continuación:

---
import BaseHead from '@components/BaseHead.astro';
import Header from '@components/Header.astro';
import Footer from '@components/Footer.astro';
import { SITE_TITLE, SITE_DESCRIPTION } from '@/consts';
import { getCollection } from 'astro:content';
import FormattedDate from '@components/FormattedDate.astro';
import type { Collection, Post } from '@/types/collections.d';  

const collection: Collection = "blog";

const posts: Post[] = (await getCollection(collection)).sort(
    (a: Post, b: Post) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
);
---
<!doctype html>
<html lang="en">
    <head>
        <BaseHead title={SITE_TITLE} description={SITE_DESCRIPTION} />
        <style>
        // líneas 19 a 85
        </style>
    </head>
    <body>
        <Header />
        <main>
            <section>
                <ul>
                    {
                        posts.map((post) => (
                            <li>
                                <a href={`/blog/${post.slug}/`}>
                                    <img width={720} height={360} src={post.data.heroImage} alt="" />
									<h4 class="title">{post.data.title}</h4>
									<p class="date">
									    <FormattedDate date={post.data.pubDate} />
									</p>
								</a>
							</li>
						))
					}
				</ul>
			</section>
		</main>
		<Footer />
    </body>
</html>

Y ya no debería “quejarse” de ningún fallo.

Una pequeña trampa

Gracias por llegar hasta aquí. En próximos artículos, seguiré contando cómo crear una plantilla personalizada.

Aunque si no quieres crearla, puedes usar la mía (la que tendrás, más o menos, si sigues todos los pasos de todos los artículos de esta serie) con el siguiente comando:

yarn create astro -- --template borjalofe/astro-blog-template