Astro Type Errors as Silent Deploy Blockers

A site can build and run perfectly in dev mode while harboring type errors that fail `astro check` in CI. If CI gates on type checking (and it should), these latent errors become deploy blockers that surface only after pushing -- never during local development.

Tags

Astro Type Errors as Silent Deploy Blockers

The Lesson

A site can build and run perfectly in dev mode while harboring type errors that fail astro check in CI. If CI gates on type checking (and it should), these latent errors become deploy blockers that surface only after pushing -- never during local development.

Context

HAx is an Astro 6.x static site using Preact for interactive islands (search, save, share buttons) and Zod for content schema validation. The CI pipeline runs npx astro check as a type-checking gate before the build step. The project was developed across 10 phases, with Preact islands and content schemas introduced in different phases by different tooling generations.

What Happened

  1. All 10 phases of development completed successfully with local builds passing (npm run build succeeded every time).
  2. On push to main, the CI pipeline ran npx astro check and failed with multiple type errors that had never appeared during local development.
  3. Error 1: Deprecated import path. content.config.ts imported z from astro:content, which was deprecated in Astro 6.x in favor of astro:schema. The build doesn't care -- Zod works either way at runtime -- but the type checker flags it.
  4. Error 2: Missing JSX pragma. All four Preact island files (SaveButton, SaveManager, SearchWidget, ShareButton) were missing the /** @jsxImportSource preact */ pragma. Astro's dev server infers the JSX runtime from the integration config, but the type checker requires the explicit pragma to resolve JSX types.
  5. Error 3: Type narrowing gaps. Two pages used .filter(Boolean) to remove nulls from getEntry() results. TypeScript doesn't narrow the type through Boolean as a filter predicate -- it needs an explicit type guard like .filter((e): e is NonNullable<typeof e> => Boolean(e)).
  6. A 7-file fix resolved all three categories: moved the import, added pragmas, and added type guards.

Key Insights

Examples

Deprecated import (builds, fails type check):

// Bad -- deprecated in Astro 6.x
import { defineCollection, z } from 'astro:content';

// Good -- canonical import path
import { defineCollection } from 'astro:content';
import { z } from 'astro:schema';

Missing JSX pragma (works in dev, fails type check):

// Bad -- Preact island without pragma
import { useState } from 'preact/hooks';
export default function SaveButton() { /* ... */ }

// Good -- explicit pragma
/** @jsxImportSource preact */
import { useState } from 'preact/hooks';
export default function SaveButton() { /* ... */ }

Type narrowing with Boolean filter:

// Bad -- TypeScript can't narrow through Boolean
const resolved = entries.filter(Boolean);
// Type: (Entry | undefined)[]

// Good -- explicit type predicate
const resolved = entries.filter((e): e is NonNullable<typeof e> => Boolean(e));
// Type: Entry[]

Applicability

This applies to any project using Astro with TypeScript, and the JSX pragma issue is specific to Preact (not React) islands. The .filter(Boolean) narrowing issue applies to all TypeScript projects. The broader principle -- "run your CI checks locally" -- is universal.

Related Lessons

Related Lessons