How to Adapt Astro to TypeScript
Astro uses TypeScript by default, so to create a blog (or any other type of website) with Astro, you can follow the steps I explained in “How to Create a Blog with Astro” while keeping in mind the changes I’ll mention below:
Step 1: Create the Project with Astro
Create a new Astro project using yarn:
yarn create astro
During the configuration process, answer the following questions (pay attention to the change in the TypeScript question):
- Where do you want to create your new project?
./astro-blog-template
(or whatever you want to name your project)
- How would you like to start your new project?
Use the blog template
- Do you plan to write in TypeScript?
Yes
- How strict should TypeScript be?
Strict (Recommended)
- Install dependencies?
Yes
- Initialize a new git repository?
Yes
Note 1:
I named the project
astro-blog-template
because I wrote this article while creating the template. Replace that name with whatever suits you best.Also, at the end of the article, I’ll explain how to use the template and get all this (and some extras I explain in other articles) right out of the box.
Note 2:
You can answer differently (of course), but I recommend using the same options as I did.
Step 2: Set Up the Repository
Just like in the article “How to Create a Blog with Astro - Step 2: Set Up the Repository”.
Step 3: Make Sure Everything Works
You can do this by opening a terminal in the project folder and running:
yarn dev
and accessing http://localhost:4321/.
Note:
Check the website after each step you complete from now on to make sure everything remains the same (i.e., that you haven’t broken the website).
yarn dev
automatically watches for changes and updates the website with them.
Step 4: Improve the Project Configuration
Just like in the article “How to Create a Blog with Astro - Step 4: Improve the Project Configuration” but keep in mind that the configuration in tsconfig.json
extends astro/tsconfigs/strict
instead of astro/tsconfigs/base
, so your configuration file (after the changes) should look like this:
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"baseUrl": "src/",
"paths": {
"@/*": ["./*"],
"@components/*": ["components/*"],
"@layouts/*": ["layouts/*"],
"@styles/*": ["styles/*"]
},
"strictNullChecks": true
}
}
Step 5: Apply Your Data to the Project
Just like in the article “How to Create a Blog with Astro - Step 5: Customize the Project Structure”.
Step 6: Fix TypeScript Errors
For many, it’s a headache. For me, it’s a small inconvenience that saves me from problems and tests later on.
Create src/types/collections.d.ts
I’m not a big fan of making complete changes, but in this case, we’re going to use the types in several places, so we’re preparing for the future.
Let’s get to it:
- Create the file
src/types/collections.d.ts
(which implies creating the foldersrc/types/
). - Write the following in the file
src/types/collections.d.ts
:import { type CollectionEntry } from 'astro:content'; export type Collection = "blog"; export type Post = CollectionEntry<'blog'>;
Note:
By convention, TypeScript files that contain (only) type definitions have the
d
(definition) suffix, which is why the file you just created is calledcollections.d.ts
to indicate that it contains the collection type definitions (from Astro’s content collections).
Why Don’t We Modify tsconfig.json
?
Do you remember the paths we created in Step 4? You might be tempted to add a new one for @types
.
You’ll likely change your tsconfig.ts
file to look like this:
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"baseUrl": "src/",
"paths": {
"@/*": ["./*"],
"@components/*": ["components/*"],
"@layouts/*": ["layouts/*"],
"@styles/*": ["styles/*"],
"@types/*": ["types/*"]
},
"strictNullChecks": true
}
}
And you’ll likely see the import error.
What Happened?
@types
is a type package prefix.
For example, if you’re going to use ReactJS with TypeScript, you’ll install @types/react
as a dependency.
That’s why if you create the path in tsconfig.ts
, the project can’t decide whether it’s an external package (in node_modules
) or an internal project definition.
How Do We Solve It?
You don’t need to do anything.
If you look at your configuration, you’ll see that you created the following line:
"@/*": ["./*"]
With that, you’re making @/
point to src/
, so you can import types (internally) with @/types/
.
Modify src/pages/blog/[...slug].astro
Initially, the file looks like this:
---
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>
And you’ll see that, on line 6, post
is marked with the error:
Parameter 'post' implicitly has an 'any' type.ts(7006)
.
And that’s because TypeScript strict (which we configured for our project) “is not a fan” of the any
type as it allows any type and is therefore susceptible to errors.
Fortunately, we’ve already created the necessary types in the file src/types/collections.d.ts
.
All that’s left is to import the types and replace them where necessary.
After the changes, the file should look like this:
---
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>
With this, the errors in this file will disappear.
Note:
In some JavaScript-based frameworks (like Astro or NextJS), we encounter folder or file names in brackets.
The closest example right now is this very file:
src/pages/blog/[...slug].astro
.This means that when processing a URL like
blog/rest-of/the-url
, Astro will take everything that comes after the part it knows (in this case/blog
) and store it in a parameter calledslug
to access that data from the file itself.You can discover more details and gain a better understanding in the official Astro documentation.
Modify src/pages/blog/index.astro
In the file src/pages/blog/index.astro
, which will serve as the blog’s main page (or blog archive page), we encounter a couple of TypeScript errors.
Below, I’ve included how the file currently looks (for convenience, I’ve removed the CSS lines), where you should see errors marked on lines 10 and 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>
// lines 19 to 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>
I say errors… but it’s actually the same error: Parameter '...' implicitly has an 'any' type.ts(7006)
.
As with the previous case (the file src/pages/blog/[...slug].astro
), we have the solution prepared with the types we defined in the file src/types/collections.d.ts
.
After the changes, the file should look like this:
---
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>
// lines 19 to 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>
And it should no longer “complain” about any errors.
A Little Trick
Thanks for making it this far. In upcoming articles, I’ll continue explaining how to create a custom template.
But if you don’t want to create one, you can use mine (the one you’ll have, more or less, if you follow all the steps in this series of articles) with the following command:
yarn create astro -- --template borjalofe/astro-blog-template