AstroShowcase
Docs / Advanced / TypeScript in Astro
Advanced Updated May 7, 2026

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):

PresetWhat it does
astro/tsconfigs/baseMinimal — just what Astro needs
astro/tsconfigs/strictRecommended — catches most bugs
astro/tsconfigs/strictestMaximum 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.