Webdesign 13 Min. Lesezeit

TypeScript Strict Mode in Astro: satisfies, Generics in Props & Collection-Typing

TypeScript strict vollständig in Astro-Projekten nutzen: das strict-Flag, der satisfies-Operator, typsichere Props mit Generics und das Typsystem der Content Collections.

TypeScript mit strict: true in einem Astro-Projekt zu betreiben bedeutet mehr als das Flag in tsconfig.json zu setzen — es verändert, wie man Props definiert, wie man Content-Collections typisiert und wo man satisfies statt as verwendet. Dieser Artikel zeigt die wichtigsten Patterns für produktionsreife Astro-Projekte.

Das strict-Flag und was es aktiviert

strict: true in der tsconfig.json ist ein Sammelflag, das mehrere TypeScript-Checks gleichzeitig aktiviert:

{
  "extends": "astro/tsconfigs/strict",
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true
  }
}

Die wichtigsten enthaltenen Checks:

FlagEffekt
strictNullChecksnull und undefined sind keine gültigen Werte für andere Typen
strictFunctionTypesFunktionsparameter werden contravariant geprüft
noImplicitAnyImplizites any ist verboten
strictPropertyInitializationKlassen-Properties müssen im Konstruktor initialisiert werden

Zusätzlich empfehlenswert: noUncheckedIndexedAccess — Array-Zugriffe per Index geben T | undefined zurück, nicht T.

const items = ['Astro', 'React', 'TypeScript'];
const first = items[0];
//    ^? string | undefined  (mit noUncheckedIndexedAccess)
//    ^? string              (ohne)

// Zwingt zu explizitem Null-Check:
if (first !== undefined) {
  console.log(first.toUpperCase());
}

Props-Interfaces in Astro-Komponenten

Astro-Komponenten nutzen interface Props oder type Props im Frontmatter-Block. Mit strict mode muss man präzise sein.

---
interface Props {
  title: string;
  description?: string;
  level?: 1 | 2 | 3 | 4 | 5 | 6;
  class?: string;
}

const {
  title,
  description,
  level = 2,
  class: className = '',
} = Astro.props;

const HeadingTag = `h${level}` as `h${typeof level}`;
---

<HeadingTag class={className}>
  {title}
  {description && <span class="text-sm font-normal text-gray-500">{description}</span>}
</HeadingTag>

Discriminated Unions für Varianten

---
type ButtonProps =
  | {
      as: 'button';
      type?: 'button' | 'submit' | 'reset';
      onClick?: () => void;
      href?: never;
    }
  | {
      as: 'a';
      href: string;
      type?: never;
      onClick?: never;
    };

type Props = ButtonProps & {
  children: string;
  variant?: 'primary' | 'secondary' | 'ghost';
  size?: 'sm' | 'md' | 'lg';
  class?: string;
};

const { as: Tag = 'button', variant = 'primary', size = 'md', class: className = '', ...rest } = Astro.props;
---

Discriminated Unions erzwingen, dass href nur bei as="a" gültig ist und type nur bei as="button" — TypeScript fängt Fehler zur Compile-Zeit.

Der satisfies-Operator

satisfies wurde in TypeScript 4.9 eingeführt und löst ein häufiges Problem: Man möchte, dass ein Objekt einem bestimmten Typ entspricht, aber TypeScript soll den spezifischsten Typen inferieren, nicht den allgemeinen.

// Ohne satisfies: TypeScript inferiert Record<string, string>
// und verliert die Information über die exakten Keys
const categoryColors = {
  BFSG: 'bg-purple-100 text-purple-700',
  SEO: 'bg-green-100 text-green-700',
  Webdesign: 'bg-blue-100 text-blue-700',
} as const;

// Mit satisfies: TypeScript prüft gegen den Record-Typ,
// behält aber die literalen Key-Namen
type Category = 'BFSG' | 'SEO' | 'Webdesign' | 'Performance' | 'News';

const categoryColors = {
  BFSG: 'bg-purple-100 text-purple-700',
  SEO: 'bg-green-100 text-green-700',
  Webdesign: 'bg-blue-100 text-blue-700',
  Performance: 'bg-orange-100 text-orange-700',
  News: 'bg-gray-100 text-gray-700',
} satisfies Record<Category, string>;

// TypeScript weiß jetzt:
// categoryColors.BFSG → 'bg-purple-100 text-purple-700' (literal type)
// categoryColors['Unbekannt'] → Fehler: Key nicht im Typ

Im Gegensatz zu as Record<Category, string> verhindert satisfies, dass fehlerhafte Werte stillschweigend akzeptiert werden.

Generics in Utility-Funktionen

// data/blog-articles.ts

export function getArticlesByCategory<T extends BlogArticle['category']>(
  category: T
): BlogArticle[] {
  return blogArticles.filter((a) => a.category === category);
}

// Stärker typisiert: gibt immer ein Array zurück, nie undefined
export function getArticleBySlug(slug: string): BlogArticle | undefined {
  return blogArticles.find((a) => a.slug === slug);
}

// Mit noUncheckedIndexedAccess korrekt:
export function getLatestArticle(): BlogArticle | undefined {
  const sorted = getPublishedArticles();
  return sorted[0]; // gibt BlogArticle | undefined zurück
}

Content Collection Typing

Astro Content Collections mit defineCollection und Zod liefern vollständige Typsicherheit:

// src/content/config.ts
import { defineCollection, z } from 'astro:content';

const blog = defineCollection({
  type: 'content',
  schema: z.object({
    title: z.string().min(10).max(80),
    description: z.string().min(50).max(160),
    pubDate: z.date(),
    category: z.enum(['Webdesign', 'SEO', 'Performance', 'BFSG', 'News']),
    tags: z.array(z.string()).min(1).max(10),
    readingTime: z.number().int().positive(),
    featured: z.boolean().optional().default(false),
    ogImage: z.string().optional(),
  }),
});

Das Zod-Schema wird zur Laufzeit validiert — fehlerhafte Frontmatter-Felder brechen den Build mit einem klaren Fehler ab, nicht mit einem stillen undefined im Template.

Inferierte Typen aus Collections

import type { CollectionEntry } from 'astro:content';

// Inferierter Typ für einen Blog-Eintrag
type BlogEntry = CollectionEntry<'blog'>;

// Utility für Props einer Artikel-Seite
interface ArticlePageProps {
  entry: BlogEntry;
}

Damit ist der Typ des entry-Objekts vollständig aus dem Schema inferiert — kein manuelles Interface nötig, das vom Schema abweichen könnte.

Häufige Strict-Mode-Fallen in Astro

1. Astro.props ohne explizites Interface

---
// FEHLER mit strict mode: implizites any
const { title } = Astro.props;

// RICHTIG:
interface Props { title: string }
const { title } = Astro.props;
---

2. Array-Zugriff bei getStaticPaths

---
export async function getStaticPaths() {
  const articles = await getCollection('blog');
  return articles.map((entry) => ({
    params: { slug: entry.slug },
    props: { entry },
  }));
}

// TypeScript weiß, dass entry vom Typ CollectionEntry<'blog'> ist
const { entry } = Astro.props;
const { Content } = await render(entry);
---

3. Optional Chaining vs. Non-null Assertion

// Non-null Assertion (!) ist mit strict mode erlaubt, aber riskant
const article = getArticleBySlug(slug)!; // Throws wenn undefined

// Besser: Optional Chaining mit explizitem Fehler-Handling
const article = getArticleBySlug(slug);
if (!article) {
  return Astro.redirect('/blog/', 301);
}
// Ab hier ist article sicher BlogArticle

tsconfig.json für Astro-Projekte

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

verbatimModuleSyntax stellt sicher, dass import type für reine Type-Imports verwendet wird — wichtig für Tree-Shaking und korrekte ESM-Ausgabe.

Weiterführende Artikel


TypeScript-Architektur für Ihr Projekt? Wender Media baut typsichere Frontend-Stacks, die auch in 12 Monaten noch wartbar sind — info@wendermedia.info.

Individuelle Beratung gewünscht?

Wender Media unterstützt Sie bei der praktischen Umsetzung — von der technischen Konzeption bis zum Launch. Schreiben Sie uns, wir antworten innerhalb von 24 Stunden.

Jetzt Beratung anfragen

Kostenlos & unverbindlich — info@wendermedia.info

Verwandte Artikel