TypeScript in Astro
How TypeScript works across .astro files, content collections, API routes, and libraries — plus common patterns and pitfalls.
TypeScript is on by default
Every new Astro project includes TypeScript with strict mode enabled.
The tsconfig.json extends Astro’s recommended preset:
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}
The three available presets (in order of strictness):
| Preset | What it does |
|---|---|
astro/tsconfigs/base | Minimal — just what Astro needs |
astro/tsconfigs/strict | Recommended — catches most bugs |
astro/tsconfigs/strictest | Maximum strictness |
Typing component props
Use an interface Props (or type Props) in the frontmatter:
---
interface Props {
title: string
count?: number // optional
variant?: 'a' | 'b' | 'c' // union literal
children?: any // for slot content (rarely needed)
}
// Destructure with defaults
const { title, count = 0, variant = 'a' } = Astro.props
---
Astro reads this interface automatically — no extra config needed. TypeScript will error if a parent passes a wrong prop type.
Typing Astro.locals (middleware)
Declare locals in src/env.d.ts:
// src/env.d.ts
/// <reference types="astro/client" />
declare namespace App {
interface Locals {
requestId: string
user: { id: number; name: string; email: string } | null
authenticated: boolean
}
}
Now Astro.locals.user is fully typed in every .astro file and API route.
Typing API routes
Use the APIRoute type for endpoint handlers:
// src/pages/api/posts.ts
import type { APIRoute } from 'astro'
import { db } from '@/lib/db'
import { sites } from '@/lib/schema'
export const GET: APIRoute = async ({ url }) => {
const page = Number(url.searchParams.get('page') ?? 1)
const limit = 20
const rows = await db.select().from(sites).limit(limit).offset((page - 1) * limit)
return new Response(JSON.stringify(rows), {
headers: { 'Content-Type': 'application/json' },
})
}
Typing content collections
Schemas defined with Zod in src/content.config.ts automatically generate TypeScript
types for every collection entry. Use CollectionEntry<'collectionName'> for the full type:
import type { CollectionEntry } from 'astro:content'
type BlogPost = CollectionEntry<'blog'>
// blogPost.data.title is string
// blogPost.data.publishedAt is Date
// blogPost.id is string
Path aliases
The @/* → src/* alias (set in tsconfig.json) means you can import from any
depth without counting ../../:
// ✗ Fragile — breaks when files move
import { db } from '../../../lib/db'
// ✓ Always correct
import { db } from '@/lib/db'
Running the type checker
npx astro check
This type-checks all .astro files (which tsc alone cannot do, since .astro
isn’t standard TypeScript). Run it in CI before deploying.
Common patterns
Nullable / optional fetched data
---
const post = await getPostBySlug(Astro.params.slug)
// TypeScript knows post could be null — redirect if so
if (!post) return Astro.redirect('/404')
// After the guard, TypeScript knows post is not null
const { title, body } = post // ✓ No "possibly undefined" error
---
Satisfies operator
Use satisfies to validate object literals without widening their type:
const config = {
theme: 'dark',
locale: 'en',
} satisfies Record<string, string>
// config.theme is 'dark' (literal), not just string
Type-safe form data
function getFormString(form: FormData, key: string): string {
const value = form.get(key)
if (typeof value !== 'string') throw new Error(`Missing form field: ${key}`)
return value.trim()
}
// Usage in frontmatter
const form = await Astro.request.formData()
const email = getFormString(form, 'email') // string, not FormDataEntryValue
astro check in CI
Add a typecheck step to your deploy pipeline:
# In deploy.sh or CI workflow
npx astro check || exit 1
npm run build
This catches type errors before they reach production.