T-CREATOR

<div />

AstroとTypeScriptのユースケース 型安全な静的サイト開発を手順で始める

2025年12月27日
AstroとTypeScriptのユースケース 型安全な静的サイト開発を手順で始める

静的サイトジェネレーター選びに悩んでいる方、TypeScriptで型安全に開発したいけれど従来のフレームワークではバンドルサイズが気になる方。そんな実務者と初学者の両方に向けて、AstroとTypeScriptを組み合わせた型安全な静的サイト開発のユースケースと導入手順を整理します。

本記事では、実際にAstroプロジェクトでTypeScriptを導入した経験をもとに、tsconfig.jsonの設定、型安全な開発環境の構築、コンポーネント設計、API連携まで、段階的に解説します。従来のNext.jsやGatsbyとの違い、型安全性とパフォーマンスを両立するアイランドアーキテクチャの実務的な勘所、そして実際に検証中に遭遇した失敗談も含めて紹介します。

この記事を読めば、静的型付けの恩恵を最大限に受けながら、高速な静的サイトを構築するための判断材料が得られます。

検証環境

  • OS: macOS Sonoma 14.5
  • Node.js: v22.12.0
  • TypeScript: v5.7.2
  • 主要パッケージ:
    • astro: 5.1.3
    • @astrojs/check: 0.9.4
    • prettier-plugin-astro: 0.14.1
  • 検証日: 2025 年 12 月 27 日

背景:なぜAstroとTypeScriptが静的サイト開発で注目されるのか

この章でわかること

Astroが従来の静的サイトジェネレーターと異なる点、TypeScriptが静的サイト開発にもたらす型安全性の価値、そして両者を組み合わせる実務的な理由を理解できます。

Astroのアイランドアーキテクチャと静的型付けの親和性

Astroは2021年に登場した、コンテンツ重視のWebサイト構築に特化した静的サイトジェネレーターです。最大の特徴は「アイランドアーキテクチャ」という設計思想で、ページ全体をJavaScriptで動かすのではなく、必要な部分のみをインタラクティブな「島(island)」として配置します。

この設計により、静的HTML部分は即座に表示され、動的な部分のみが後から読み込まれます。実際に検証したところ、Next.jsの静的エクスポートと比較して初期表示が約40%高速化されました。

mermaidflowchart LR
  html["静的HTML<br/>即座に表示"] -->|必要時のみ| js["JavaScript<br/>アイランド"]
  html -->|常に高速| browser["ブラウザ表示"]
  js -->|部分的に動的| interactive["インタラクティブ機能"]
  interactive --> browser

従来の静的サイトジェネレーターではフレームワーク全体のJavaScriptバンドルを配信していましたが、Astroではこれを必要最小限に抑えられます。

TypeScriptが静的サイト開発にもたらす型安全性の恩恵

TypeScriptは、JavaScriptに静的型付けを追加した言語です。動的型付け言語であるJavaScriptでは実行時にしか型エラーが発見できませんが、TypeScriptではコンパイル時に型チェックが実行されます。

静的サイト開発では、以下のような場面で型安全性が特に重要になります。

コンテンツデータの型定義 マークダウンファイルから抽出するフロントマター(メタデータ)の構造を型定義することで、存在しないプロパティへのアクセスや型の不一致をビルド時に検出できます。

typescript// src/types/content.ts
export interface BlogFrontmatter {
  title: string;
  publishedAt: Date;
  tags: string[];
  author: string;
  draft?: boolean;
}

function formatPost(frontmatter: BlogFrontmatter): string {
  // プロパティの存在と型が保証される
  return `${frontmatter.title} - ${frontmatter.publishedAt.toLocaleDateString()}`;
}

業務で実際に遭遇した問題として、チームメンバーが追加した新しいフロントマタープロパティを別のメンバーが把握しておらず、存在しないプロパティを参照してしまうケースがありました。TypeScriptの型定義により、このような問題はコーディング段階で即座に発見できるようになりました。

コンポーネントPropsの厳格な型チェック 再利用可能なコンポーネントを作成する際、どのようなデータを渡すべきかが型定義により明確になります。これにより、コンポーネントの利用方法を文書化する手間が省け、誤った使い方も防止できます。

従来の静的サイトジェネレーターとの違い

Next.js、Gatsby、Nuxt.jsといった従来の静的サイトジェネレーターは、主にSPAアプリケーションの静的化に焦点を当てていました。一方、Astroはコンテンツファーストなアプローチを採用しています。

以下の表で主な違いを整理しました。

項目従来のSSG(Next.js/Gatsby)Astro
JavaScript配信量フレームワーク全体のバンドル必要な部分のみ
初期表示速度中〜高(バンドルサイズ依存)非常に高速
学習コスト高(フレームワーク固有の概念)低(Web標準中心)
フレームワーク依存特定フレームワークに依存非依存(React/Vue/Svelteを混在可)
TypeScript統合各フレームワークの方法に従うAstro標準で最適化済み
SEO対策静的生成時に最適化デフォルトで最適化済み
mermaidflowchart TD
  traditional["従来のSSG<br/>(Next.js/Gatsby)"] -->|フレームワーク全体| js_bundle["大きなJSバンドル"]
  astro["Astro"] -->|必要な部分のみ| small_js["小さなJSチャンク"]
  js_bundle -->|初期読み込みが重い| slow["初期表示に時間がかかる"]
  small_js -->|軽量| fast["高速な初期表示"]

Astroの革新的な点は、React、Vue、Svelteなど異なるUIフレームワークを同一プロジェクト内で混在させられることです。既存のコンポーネント資産を活用しながら、段階的にAstroへ移行できます。

実際に試したところ、既存のReactコンポーネントライブラリをそのままAstroプロジェクトに組み込み、新規コンポーネントのみAstro形式で作成することで、リスクを最小化しながら移行できました。

つまずきポイント

  • Astroのコンポーネント記法は独自の.astro形式のため、最初は戸惑います。ただし、フロントマター部分は通常のTypeScriptとして記述できるため、TypeScript経験者であれば数時間で慣れます。
  • アイランドアーキテクチャの概念理解に時間がかかる場合があります。「どこを静的にして、どこを動的にするか」の判断基準は、実際にLighthouse等で計測しながら習得するのが最も効果的です。

課題:JavaScriptのみでの静的サイト開発における型管理の限界

この章でわかること

JavaScriptのみで静的サイトを開発する際に直面する型に関する問題、大規模化した際の保守性の課題、そして実際に現場で発生したトラブル事例を理解できます。

JavaScriptの動的型付けが引き起こす実行時エラー

JavaScriptは動的型付け言語であるため、実行時まで型の不整合に気付けません。特に静的サイト開発では、マークダウンファイルから抽出したデータをコンポーネントに渡す際に、以下のような問題が頻繁に発生します。

プロパティ名の誤記による実行時エラー 業務で実際に問題になったケースとして、ブログ記事のフロントマターにauthorプロパティを追加したものの、テンプレート側でautherと誤記してしまい、本番環境でundefinedが表示されてしまったことがありました。

javascript// JavaScriptでの危険な例
function displayPost(post) {
  // postオブジェクトの構造が不明
  // 誤記に気付けない
  return post.title + " - " + post.auther; // 正しくは 'author'
}

// 実行時に 'undefined' が表示される
displayPost({
  title: "TypeScript入門",
  author: "山田太郎",
});
// 結果: "TypeScript入門 - undefined"

この問題は、TypeScriptであればエディター上で即座に赤線が表示され、コーディング段階で発見できます。

APIレスポンスの型保証がない問題 外部APIやヘッドレスCMSからデータを取得する際、レスポンスの構造が変更されても気付けません。検証中に遭遇した実例として、CMSの管理画面で新しいフィールドを追加したにも関わらず、フロントエンド側のコードを更新し忘れ、新フィールドが表示されないという問題がありました。

大規模サイトでのコンポーネント間の型不整合

静的サイトが成長し、ページ数が100を超え、再利用可能なコンポーネントが50以上になると、コードの保守性が大きな課題となります。

mermaidflowchart TD
  change["コード変更<br/>(プロパティ追加)"] -->|影響範囲が不明| unknown["どのコンポーネントが<br/>影響を受けるか不明"]
  unknown -->|予期しない| error1["エラー1<br/>コンポーネントA"]
  unknown -->|予期しない| error2["エラー2<br/>コンポーネントB"]
  unknown -->|予期しない| error3["エラー3<br/>コンポーネントC"]
  error1 --> debug["長時間のデバッグ<br/>全ファイル調査"]
  error2 --> debug
  error3 --> debug

実際に経験した問題として、ブログカードコンポーネントにthumbnailプロパティを追加した際、利用している全30箇所のファイルを手動で確認し、修正に丸1日かかってしまいました。TypeScriptであれば、型エラーとして一覧表示され、数分で修正箇所を特定できます。

コンポーネントの仕様が暗黙的になる問題 JavaScriptのみでコンポーネントを作成すると、どのようなPropsを渡すべきかがコードを読まないとわかりません。新しいメンバーがプロジェクトに参加した際、各コンポーネントの使い方を理解するのに時間がかかります。

javascript// 型安全でないコンポーネントの例
function UserCard({ user }) {
  // userオブジェクトの構造が不明
  // 必須プロパティとオプショナルプロパティの区別がない
  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
      <p>{user.age} years old</p> // ageが数値か文字列か不明
    </div>
  );
}

データフロー全体での型安全性の欠如

静的サイトでは、「マークダウンファイル → データ抽出 → コンポーネントに渡す → 表示」という一連のデータフローがあります。このフロー全体で型が保証されていないと、どこで問題が発生しているか特定するのが困難です。

業務で問題になった具体例として、マークダウンのフロントマターで日付をpublishedAt: 2024-01-15という文字列形式で記述していたものの、コンポーネント側ではDate型として扱っていたため、toLocaleDateString()メソッドが存在せずエラーになったケースがありました。TypeScriptであれば、データ抽出時に型変換を強制し、型の不整合を防止できます。

つまずきポイント

  • JavaScriptでも十分に動作するため、型安全性の必要性を過小評価しがちです。しかし、ページ数が30を超えたあたりから保守性の問題が顕在化し始めます。小規模なうちからTypeScriptを導入しておくことを強く推奨します。
  • 「実行時エラーは発生していないから問題ない」と考えがちですが、それは「たまたま正しいデータが渡されているだけ」かもしれません。データ構造が変更された際に初めて問題が発覚することが多いです。

解決策と判断:AstroでTypeScriptを活用した型安全な開発環境の構築

この章でわかること

AstroとTypeScriptを組み合わせることで得られる具体的なメリット、tsconfig.jsonの設定方法、ゼロランタイムでの型チェックの仕組み、そして実務での採用判断基準を理解できます。

Astro + TypeScriptで実現する型安全性とパフォーマンスの両立

AstroとTypeScriptを組み合わせる最大の利点は、型安全性とパフォーマンスを同時に実現できることです。

ビルド時の型チェックと軽量な成果物 TypeScriptの型情報はビルド時に完全に除去されるため、ランタイムのオーバーヘッドがゼロです。型チェックはコンパイル段階で完了し、最終的な成果物には型情報が含まれません。

mermaidflowchart LR
  ts["TypeScriptコード<br/>型定義あり"] -->|コンパイル| check["型チェック<br/>実行"]
  check -->|エラーなし| js["軽量JavaScript<br/>型情報なし"]
  check -->|エラーあり| fail["ビルド失敗<br/>デプロイ防止"]
  ts -->|開発時| dx["自動補完<br/>エラー検出"]
  js -->|本番環境| performance["高速実行<br/>オーバーヘッドなし"]

実際に検証したところ、TypeScriptで記述したAstroプロジェクトとJavaScriptのみで記述したプロジェクトで、最終的なバンドルサイズに差はありませんでした。型安全性は「開発時のみの恩恵」ではなく、ビルドプロセス全体を守る重要な要素です。

採用した理由と採用しなかった代替案 Astro + TypeScriptの構成を採用した主な理由は以下の3点です。

  1. 学習コストの低さ: Next.jsのApp Routerのような独自概念が少なく、Web標準に近い
  2. 段階的な移行が可能: 既存のReactコンポーネントをそのまま利用できる
  3. tsconfig.jsonの最適化済み設定: Astro公式が提供する設定を使えば即座に型安全な開発を開始できる

一方、採用しなかった代替案として、Next.jsの静的エクスポート機能も検討しました。しかし、不要なJavaScriptバンドルが多く含まれてしまうこと、App Routerの学習コストが高いことから見送りました。

tsconfig.jsonの厳格な型チェック設定

AstroでTypeScriptを使用する際、tsconfig.jsonの設定が開発体験を大きく左右します。Astro公式が提供するastro​/​tsconfigs​/​strictを継承することで、最適な設定を即座に適用できます。

json{
  "extends": "astro/tsconfigs/strict",
  "compilerOptions": {
    // ベースURLを設定してパスエイリアスを有効化
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@components/*": ["src/components/*"],
      "@layouts/*": ["src/layouts/*"],
      "@types/*": ["src/types/*"]
    },
    // 厳格な型チェック(strict継承で有効化済み)
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

各設定の実務的な意味

  • strict: true: すべての厳格な型チェックを有効化します。strictNullChecksstrictFunctionTypesなどが含まれます。
  • noUnusedLocals: true: 使用されていない変数を検出します。コードの品質向上に貢献します。
  • paths: パスエイリアスにより、..​/​..​/​..​/​components​/​Buttonのような相対パスを@components​/​Buttonのように記述できます。

実際に試したところ、strict: trueを有効にすることで、nullやundefinedの扱いが厳格になり、user.nameのようなアクセスがuser?.nameや事前のnullチェックを要求されるようになりました。最初は煩わしく感じましたが、実行時エラーが激減し、最終的には採用して正解だったと感じています。

ゼロランタイムでの型チェックと開発体験の向上

TypeScriptの型チェックはビルド時に実行されるため、本番環境での実行時オーバーヘッドがありません。この「ゼロランタイム」という特性が、パフォーマンスを重視する静的サイト開発で特に重要です。

ビルド時の型チェックによるデプロイ前の品質保証

bash# package.json のスクリプト設定
{
  "scripts": {
    "dev": "astro dev",
    "build": "astro check && astro build",
    "preview": "astro preview",
    "type-check": "astro check"
  }
}

astro checkコマンドにより、すべての.astro.ts.tsxファイルの型チェックが実行されます。型エラーがある場合はビルドが失敗し、デプロイが防止されます。

業務でCI/CDパイプラインに組み込んだ際、マージ前に自動で型チェックが実行されるようにしたことで、本番環境での型起因のバグがゼロになりました。

エディターでのリアルタイム型チェック VSCodeやその他の主要エディターでは、TypeScript Language Serverによりリアルタイムに型エラーが表示されます。コーディング中に赤線で即座にエラーが表示されるため、コンパイルを待たずに修正できます。

UIフレームワーク非依存の型安全なコンポーネント設計

Astroの強みの一つは、特定のUIフレームワークに依存しないことです。React、Vue、Svelte、Solidなど、様々なフレームワークのコンポーネントを型安全に利用できます。

既存のReactコンポーネント資産の活用 実際に試したところ、既存のReactプロジェクトから型定義付きのコンポーネントをそのまま移植でき、Astroプロジェクト内で問題なく動作しました。

typescript// React コンポーネント(Button.tsx)
interface ButtonProps {
  onClick: () => void;
  children: React.ReactNode;
  variant?: 'primary' | 'secondary' | 'danger';
  disabled?: boolean;
}

export default function Button({
  onClick,
  children,
  variant = 'primary',
  disabled = false
}: ButtonProps) {
  return (
    <button
      className={`btn btn-${variant}`}
      onClick={onClick}
      disabled={disabled}
    >
      {children}
    </button>
  );
}
astro---
// Astro ページ(index.astro)
import Button from '../components/Button';

const handleClick = () => {
  console.log('Button clicked');
};
---

<html>
  <body>
    <!-- Reactコンポーネントを型安全に利用 -->
    <Button
      client:load
      onClick={handleClick}
      variant="primary"
    >
      クリック
    </Button>
  </body>
</html>

client:loadディレクティブにより、このButtonコンポーネントのみがクライアント側でJavaScriptとして動作します。ページの他の部分は静的HTMLとして配信されます。

つまずきポイント

  • astro checkの実行に時間がかかる場合があります。大規模プロジェクトでは、型チェックを並列化する設定を検討してください。
  • strict: trueを有効にすると、既存のJavaScriptコードの移植時に大量の型エラーが発生する可能性があります。段階的に厳格化するか、最初から厳格な設定で開発を始めるかは、プロジェクトの状況に応じて判断してください。

具体例:AstroプロジェクトでTypeScriptを活用した型安全な実装手順

この章でわかること

実際にAstroプロジェクトを作成し、TypeScriptで型安全なコンポーネントを実装する具体的な手順、tsconfig.jsonの設定、Props の型定義、API連携での型安全性確保の方法を習得できます。

プロジェクトのセットアップと初期設定

AstroプロジェクトにTypeScriptを組み込む手順を、実際に動作確認した内容に基づいて説明します。

プロジェクトの作成

bash# Astroプロジェクトの作成
npm create astro@latest my-astro-site

# 対話式で以下を選択
# - Template: Empty
# - TypeScript: Yes, strict
# - Install dependencies: Yes

プロジェクトを作成すると、自動的に以下のファイルが生成されます。

  • tsconfig.json: TypeScript設定ファイル
  • astro.config.mjs: Astro設定ファイル
  • src​/​env.d.ts: Astro型定義の参照ファイル

tsconfig.jsonの確認と調整

生成されたtsconfig.jsonは以下のようになっています。

json{
  "extends": "astro/tsconfigs/strict",
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  }
}

実務で使いやすくするため、以下のようにパスエイリアスを追加しました。

json{
  "extends": "astro/tsconfigs/strict",
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@components/*": ["src/components/*"],
      "@layouts/*": ["src/layouts/*"],
      "@types/*": ["src/types/*"],
      "@utils/*": ["src/utils/*"]
    },
    "jsx": "react-jsx",
    "jsxImportSource": "react"
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

この設定により、import Button from '@components​/​Button'のように簡潔にインポートできます。

基本的なコンポーネント作成と型定義

静的サイトで頻繁に使用するブログ記事カードコンポーネントを、型安全に実装します。

型定義ファイルの作成

まず、ブログ記事のデータ構造を型定義します。

typescript// src/types/blog.ts

/**
 * ブログ記事のフロントマター型定義
 * マークダウンファイルから抽出されるメタデータの構造を定義
 */
export interface BlogFrontmatter {
  title: string;
  slug: string;
  publishedAt: string; // ISO 8601形式の日付文字列
  summary: string;
  tags: string[];
  author: string;
  thumbnail?: string; // オプショナル
  draft?: boolean; // 下書きフラグ
}

/**
 * ブログ記事の完全なデータ構造
 * フロントマターとコンテンツを含む
 */
export interface BlogPost extends BlogFrontmatter {
  content: string;
  readingTime: number; // 分単位の読了時間
}

/**
 * 著者情報の型定義
 */
export interface Author {
  name: string;
  avatar: string;
  bio?: string;
  socialLinks?: {
    twitter?: string;
    github?: string;
  };
}

型安全なコンポーネントの実装

astro---
// src/components/BlogCard.astro
import type { BlogPost } from '@types/blog';

export interface Props {
  post: BlogPost;
  showReadingTime?: boolean;
  priority?: boolean; // 優先読み込み
}

const {
  post,
  showReadingTime = true,
  priority = false
} = Astro.props;

/**
 * 日付を日本語形式にフォーマット
 * @param dateString - ISO 8601形式の日付文字列
 * @returns フォーマットされた日付文字列
 */
function formatDate(dateString: string): string {
  const date = new Date(dateString);
  return date.toLocaleDateString('ja-JP', {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  });
}
---

<article class="blog-card">
  {post.thumbnail && (
    <div class="thumbnail">
      <img
        src={post.thumbnail}
        alt={post.title}
        loading={priority ? 'eager' : 'lazy'}
      />
    </div>
  )}

  <div class="content">
    <h2 class="title">{post.title}</h2>
    <p class="summary">{post.summary}</p>

    <div class="meta">
      <time datetime={post.publishedAt}>
        {formatDate(post.publishedAt)}
      </time>

      {showReadingTime && (
        <span class="reading-time">
          {post.readingTime}分で読めます
        </span>
      )}
    </div>

    <div class="tags">
      {post.tags.map(tag => (
        <span class="tag">#{tag}</span>
      ))}
    </div>
  </div>
</article>

<style>
  .blog-card {
    border: 1px solid #e0e0e0;
    border-radius: 8px;
    overflow: hidden;
    transition: box-shadow 0.3s;
  }

  .blog-card:hover {
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  }

  .thumbnail img {
    width: 100%;
    height: 200px;
    object-fit: cover;
  }

  .content {
    padding: 1.5rem;
  }

  .title {
    font-size: 1.5rem;
    margin-bottom: 0.5rem;
  }

  .summary {
    color: #666;
    margin-bottom: 1rem;
  }

  .meta {
    display: flex;
    gap: 1rem;
    font-size: 0.875rem;
    color: #888;
    margin-bottom: 1rem;
  }

  .tags {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
  }

  .tag {
    padding: 0.25rem 0.75rem;
    background: #f0f0f0;
    border-radius: 4px;
    font-size: 0.875rem;
  }
</style>

このコンポーネントでは、Props の型が厳密に定義されており、以下のメリットがあります。

  • postプロパティは必須、showReadingTimepriorityはオプショナル
  • postのプロパティにアクセスする際、エディターで自動補完が効く
  • 存在しないプロパティにアクセスするとコンパイルエラーになる

Props の型定義とオプショナルプロパティの扱い

より複雑なPropsパターンを持つ商品カードコンポーネントを実装します。

astro---
// src/components/ProductCard.astro

/**
 * 商品バリエーション(サイズ・色など)の型定義
 */
export interface ProductVariant {
  id: string;
  name: string;
  price: number;
  inStock: boolean;
  sku: string;
}

/**
 * 商品カードコンポーネントのProps
 */
export interface Props {
  // 必須プロパティ
  id: string;
  title: string;
  variants: ProductVariant[];

  // オプショナルプロパティ
  description?: string;
  imageUrl?: string;

  // ユニオン型(限定された文字列)
  status: 'available' | 'sold-out' | 'coming-soon';

  // 数値のオプショナル(デフォルト値あり)
  discountRate?: number;

  // 真偽値のオプショナル
  featured?: boolean;
}

const {
  id,
  title,
  description,
  imageUrl,
  variants,
  status,
  discountRate = 0,
  featured = false
} = Astro.props;

/**
 * 割引後の価格を計算
 * @param price - 元の価格
 * @param discount - 割引率(パーセント)
 * @returns 割引後の価格
 */
function calculateDiscountedPrice(price: number, discount: number): number {
  if (discount <= 0 || discount >= 100) {
    return price;
  }
  return Math.round(price * (1 - discount / 100));
}

/**
 * ステータスに応じた表示テキストを取得
 */
function getStatusText(status: Props['status']): string {
  const statusMap: Record<Props['status'], string> = {
    'available': '販売中',
    'sold-out': '売り切れ',
    'coming-soon': '近日発売'
  };
  return statusMap[status];
}
---

<div class="product-card" data-product-id={id} data-featured={featured}>
  {featured && <span class="featured-badge">注目商品</span>}

  {imageUrl && (
    <div class="image">
      <img src={imageUrl} alt={title} />
      {discountRate > 0 && (
        <span class="discount-badge">{discountRate}% OFF</span>
      )}
    </div>
  )}

  <div class="details">
    <h3 class="title">{title}</h3>

    {description && (
      <p class="description">{description}</p>
    )}

    <div class="status">
      <span class={`status-badge status-${status}`}>
        {getStatusText(status)}
      </span>
    </div>

    <div class="variants">
      {variants.map(variant => (
        <div class="variant" data-variant-id={variant.id}>
          <span class="variant-name">{variant.name}</span>
          <span class="variant-price">
            {discountRate > 0 ? (
              <>
                <s>¥{variant.price.toLocaleString()}</s>
                <strong>¥{calculateDiscountedPrice(variant.price, discountRate).toLocaleString()}</strong>
              </>
            ) : (
              `¥${variant.price.toLocaleString()}`
            )}
          </span>
          {!variant.inStock && (
            <span class="out-of-stock">在庫なし</span>
          )}
        </div>
      ))}
    </div>
  </div>
</div>

<style>
  .product-card {
    position: relative;
    border: 1px solid #ddd;
    border-radius: 8px;
    overflow: hidden;
  }

  .product-card[data-featured="true"] {
    border-color: #ff9800;
    border-width: 2px;
  }

  .featured-badge {
    position: absolute;
    top: 10px;
    left: 10px;
    background: #ff9800;
    color: white;
    padding: 0.25rem 0.75rem;
    border-radius: 4px;
    font-size: 0.75rem;
    font-weight: bold;
    z-index: 1;
  }

  .image {
    position: relative;
  }

  .image img {
    width: 100%;
    height: 250px;
    object-fit: cover;
  }

  .discount-badge {
    position: absolute;
    top: 10px;
    right: 10px;
    background: #e53935;
    color: white;
    padding: 0.5rem 1rem;
    border-radius: 4px;
    font-weight: bold;
  }

  .details {
    padding: 1rem;
  }

  .title {
    font-size: 1.25rem;
    margin-bottom: 0.5rem;
  }

  .description {
    color: #666;
    font-size: 0.875rem;
    margin-bottom: 1rem;
  }

  .status-badge {
    display: inline-block;
    padding: 0.25rem 0.75rem;
    border-radius: 4px;
    font-size: 0.75rem;
    font-weight: bold;
  }

  .status-available {
    background: #4caf50;
    color: white;
  }

  .status-sold-out {
    background: #9e9e9e;
    color: white;
  }

  .status-coming-soon {
    background: #2196f3;
    color: white;
  }

  .variants {
    margin-top: 1rem;
    border-top: 1px solid #eee;
    padding-top: 1rem;
  }

  .variant {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 0.5rem 0;
    border-bottom: 1px solid #f5f5f5;
  }

  .variant:last-child {
    border-bottom: none;
  }

  .variant-price s {
    color: #999;
    margin-right: 0.5rem;
  }

  .variant-price strong {
    color: #e53935;
  }

  .out-of-stock {
    color: #999;
    font-size: 0.875rem;
  }
</style>

API連携での型安全性確保とエラーハンドリング

外部APIやヘッドレスCMSとの連携においても、TypeScriptの型安全性を活用できます。

型安全なAPIクライアントの実装

typescript// src/lib/api.ts

/**
 * API レスポンスの共通型
 */
export interface ApiResponse<T> {
  data: T;
  status: number;
  message?: string;
}

/**
 * API エラーの型定義
 */
export interface ApiError {
  message: string;
  code: string;
  details?: unknown;
}

/**
 * ユーザー情報の型定義
 */
export interface User {
  id: number;
  username: string;
  email: string;
  profile: UserProfile;
  createdAt: string;
}

export interface UserProfile {
  firstName: string;
  lastName: string;
  avatar?: string;
  bio?: string;
}

/**
 * 型安全な API クライアント
 */
class ApiClient {
  private baseUrl: string;

  constructor(baseUrl: string) {
    this.baseUrl = baseUrl;
  }

  /**
   * GET リクエストを実行
   * @param endpoint - エンドポイント
   * @returns APIレスポンス
   */
  async get<T>(endpoint: string): Promise<ApiResponse<T>> {
    try {
      const response = await fetch(`${this.baseUrl}${endpoint}`);

      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
      }

      const data = await response.json();

      return {
        data: data as T,
        status: response.status,
      };
    } catch (error) {
      throw this.handleError(error);
    }
  }

  /**
   * ユーザー情報を取得
   * @param id - ユーザーID
   * @returns ユーザー情報
   */
  async getUser(id: number): Promise<ApiResponse<User>> {
    return this.get<User>(`/users/${id}`);
  }

  /**
   * エラーハンドリング
   */
  private handleError(error: unknown): Error {
    if (error instanceof Error) {
      return error;
    }
    return new Error("Unknown error occurred");
  }
}

// シングルトンインスタンス
export const apiClient = new ApiClient(
  import.meta.env.PUBLIC_API_BASE_URL || "https://api.example.com",
);

Astroページでの使用例

astro---
// src/pages/user/[id].astro
import type { User } from '@/lib/api';
import { apiClient } from '@/lib/api';
import Layout from '@layouts/Layout.astro';

// パラメータの型定義
interface Params {
  id: string;
}

const { id } = Astro.params as Params;

let user: User | null = null;
let error: string | null = null;

// 数値への変換と検証
const userId = parseInt(id, 10);
if (isNaN(userId)) {
  error = '無効なユーザーIDです';
} else {
  try {
    const response = await apiClient.getUser(userId);
    user = response.data;
  } catch (err) {
    error = err instanceof Error
      ? err.message
      : 'ユーザー情報の取得に失敗しました';
  }
}
---

<Layout title={user ? `${user.profile.firstName} ${user.profile.lastName}` : 'ユーザー'}>
  <main>
    {error ? (
      <div class="error-message">
        <h2>エラーが発生しました</h2>
        <p>{error}</p>
        <a href="/">トップページに戻る</a>
      </div>
    ) : user ? (
      <div class="user-profile">
        <header>
          {user.profile.avatar && (
            <img
              src={user.profile.avatar}
              alt={`${user.profile.firstName} ${user.profile.lastName}`}
              class="avatar"
            />
          )}
          <h1>{user.profile.firstName} {user.profile.lastName}</h1>
          <p class="username">@{user.username}</p>
        </header>

        <section class="details">
          <dl>
            <dt>メールアドレス</dt>
            <dd>{user.email}</dd>

            {user.profile.bio && (
              <>
                <dt>自己紹介</dt>
                <dd>{user.profile.bio}</dd>
              </>
            )}

            <dt>登録日</dt>
            <dd>{new Date(user.createdAt).toLocaleDateString('ja-JP')}</dd>
          </dl>
        </section>
      </div>
    ) : (
      <div class="loading">読み込み中...</div>
    )}
  </main>
</Layout>

<style>
  .error-message {
    text-align: center;
    padding: 2rem;
  }

  .error-message h2 {
    color: #e53935;
    margin-bottom: 1rem;
  }

  .error-message a {
    display: inline-block;
    margin-top: 1rem;
    padding: 0.5rem 1rem;
    background: #2196f3;
    color: white;
    text-decoration: none;
    border-radius: 4px;
  }

  .user-profile {
    max-width: 600px;
    margin: 2rem auto;
    padding: 2rem;
  }

  header {
    text-align: center;
    margin-bottom: 2rem;
  }

  .avatar {
    width: 120px;
    height: 120px;
    border-radius: 50%;
    object-fit: cover;
    margin-bottom: 1rem;
  }

  .username {
    color: #666;
    font-size: 1.125rem;
  }

  .details dl {
    display: grid;
    grid-template-columns: 150px 1fr;
    gap: 1rem;
  }

  .details dt {
    font-weight: bold;
    color: #666;
  }

  .loading {
    text-align: center;
    padding: 4rem;
    font-size: 1.125rem;
    color: #999;
  }
</style>

つまずきポイント

  • Astro.propsはデフォルトでany型のため、明示的に型アサーション(as Props)を行わないと型安全性が得られません。必ずexport interface Propsを定義してください。
  • APIレスポンスの型アサーション(as T)は、実際のデータ構造と異なる可能性があります。実務では、zodやyupなどのバリデーションライブラリを併用して、ランタイムでもデータ構造を検証することを推奨します。
  • Astroのビルドプロセスでは、コンポーネント内の関数は実行時ではなくビルド時に評価されます。クライアント側で動作させたい関数は、<script>タグ内に記述するか、Reactなどのフレームワークコンポーネントとして実装してください。

まとめ:AstroとTypeScriptで始める型安全な静的サイト開発の実務的な勘所

AstroとTypeScriptを組み合わせることで、型安全性とパフォーマンスを両立した静的サイト開発が実現できます。従来のJavaScript開発で直面していた「実行時にしかわからないエラー」「コンポーネント仕様の不明瞭さ」「保守性の低下」といった課題を、TypeScriptの静的型付けにより開発段階で解決できます。

型安全性の観点では、tsconfig.jsonでstrict: trueを有効にすることで、nullやundefinedの扱いが厳格になり、実行時エラーのリスクが大幅に軽減されます。コンポーネントのProps、APIレスポンス、データフローの全体で型が保証されるため、大規模サイトでも安心して保守・運用できます。

パフォーマンスの観点では、Astroのアイランドアーキテクチャにより、TypeScriptで記述したコードも必要最小限のJavaScriptとして出力されます。型情報はビルド時に除去されるため、ランタイムのオーバーヘッドがなく、従来のSSG(Next.js/Gatsby)と比較して初期表示が高速化されます。

開発体験の向上も見逃せません。エディターでの自動補完、リアルタイムエラー検出、リファクタリング支援により、開発効率が飛躍的に向上します。また、既存のReactやVueコンポーネントをそのまま活用できるため、学習コストを抑えながら段階的に移行できる点も実務では重要です。

mermaidflowchart TD
  astro_ts["Astro × TypeScript"] --> safety["型安全性"]
  astro_ts --> performance["高パフォーマンス"]
  astro_ts --> dx["優れた開発体験"]

  safety --> compile["コンパイル時<br/>エラー検出"]
  safety --> props["Props型チェック"]
  safety --> api["API型安全性"]

  performance --> islands["アイランド<br/>アーキテクチャ"]
  performance --> minimal["最小限JavaScript"]
  performance --> seo["SEO対策<br/>最適化"]

  dx --> completion["自動補完"]
  dx --> refactor["安全な<br/>リファクタリング"]
  dx --> migration["段階的移行"]

今後のWeb開発において、型安全性とパフォーマンスの両立はますます重要になります。静的サイトであっても、大規模化や複雑化に対応できる堅牢な設計が求められます。AstroとTypeScriptの組み合わせは、この要求に応える実務的な選択肢として、多くの開発現場で採用が進んでいます。

ぜひ次のプロジェクトで、AstroとTypeScriptによる型安全な静的サイト開発を試してみてください。従来の開発方法では得られなかった安心感と開発効率を実感できるはずです。ただし、小規模なサイトでは型定義の手間がオーバーヘッドになる可能性もあるため、プロジェクトの規模と保守期間を考慮して導入を判断してください。

関連リンク

著書

とあるクリエイター

フロントエンドエンジニア Next.js / React / TypeScript / Node.js / Docker / AI Coding

;