TypeScript入門実践編①Laravel+Blade+TypeScriptでデモを作ってみた!

現場で最速でつかえるTypeScript入門

第1弾はLaravel+Blade+TypeScript。Laravelの現場で出会う可能性が高い組み合わせです。Laravelと組み合わせていますが、シンプルにTypeScriptをJavaScriptにコンパイルして、そのJavaScriptファイルを組み込むというシンプルな内容です。

題材はproductsテーブル(id, name, price)を使った、ごくシンプルな商品編集ページ。表示して、保存を押すと更新される、それだけです。機能を絞ることで、構成そのものの違いに集中できるようにしています。

※内容的にはLaravelの解説が長くなってしまいました。TypeScriptの環境だけ知りたいという方は一気に9章からお読みください。

この記事の対象読者

  • 前回記事(20選)を読んで、実際の構築イメージを掴みたい人
  • 同じ処理を複数の構成で作った場合の違いを知りたい人
  • 自分がこれから学ぶ構成を、他の選択肢と比較して選びたい人

※この記事は、3つのAIに協力してもらいながら著者が慎重に裏取りしながら監修したものですが、実情とは異なるものも含まれているかもしれません。その際は、ぜひお問い合わせよりご連絡いただければ幸いです。

手順

#手順内容使うツール
1プロジェクト作成composer create-projectでLaravelをdemo_laravel_tsに作成Composer
2.env設定DB接続情報をXAMPPのMySQL(MariaDB)に合わせて編集テキストエディタ
3マイグレーション作成・実行productsテーブル(id, name, price)のマイグレーションファイルを作成し、実行してテーブルを作成Artisan CLI
4シーディング(仮データ投入)編集画面の動作確認用に、1〜2件のサンプル商品データを投入Artisan CLI (Seeder)
5ルーティング作成routes/web.phpに商品編集ページ用のルートを追加コードエディタ
6モデルを作成DBに直接アクセスし商品データの取得、更新処理などを行うモデルを作成Artisan CLI + コードエディタ
7コントローラー作成商品の表示・更新処理を行うコントローラーを作成Artisan CLI + コードエディタ
8Bladeビュー作成商品編集フォームのHTML(Blade)を作成コードエディタ
9TypeScript導入npm install、tsconfig.json設定、TSファイル作成npm
10TSでフォームの挙動を実装簡単な入力チェック等、素のTSで画面側の処理を書くコードエディタ
11動作確認ブラウザで表示→編集→保存の一連の流れを確認ブラウザ

事前準備

実装に入る前に以下の準備をお願いします。

1.XAMPPのインストール
2.MySQL(MariaDB)のインストール
3.Composerのインストール(バージョン確認:composer -V)
4.VS Codeのインストール
5.Node.jsのインストール(バージョン確認:node -vnpm -v)

実装手順解説

1.プロジェクト作成

それでは、C:\xampp8.2.4\htdocsの中に、demo_laravel_tsという名前でLaravelプロジェクトを作成します。

手順

1.VS Codeを開きます。

2.「ファイル」→「フォルダーを開く」を選択し、C:\xampp8.2.4\htdocs を指定して開きます。

3.上部メニューの「ターミナル」→「新しいターミナル」を選択し、ターミナルを開きます。

4.以下のコマンドを実行します。

composer create-project laravel/laravel demo_laravel_ts
  1. 数分待つ(パッケージのダウンロード・展開が行われます)

実行後、htdocsの中身はこうなります。

2..env設定

先ほどの手順で、LaravelはデフォルトのままだとSQLiteを使う設定になっています。今回はXAMPPのMySQL(MariaDB)を使いたいので、.envファイルを書き換えます。

1.VS Codeの左側エクスプローラーから、demo_laravel_tsフォルダ直下にある.envファイルを開きます。

2.以下のような記述があります(SQLite用の初期設定)。

DB_CONNECTION=sqlite
# DB_HOST=127.0.0.1
# DB_PORT=3306
# DB_DATABASE=laravel
# DB_USERNAME=root
# DB_PASSWORD=

3.これを、以下のようにMySQL用に書き換えます。

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=ts_demo
DB_USERNAME=root
DB_PASSWORD=

3.マイグレーション作成・実行

productsテーブルを作成します。Laravelでは、php artisan make:migrationコマンドで、テーブル定義用のファイルをまず作成します。

1.VS Codeのターミナルで、以下を実行します。

php artisan make:migration create_products_table

これにより、database/migrationsフォルダの中に、日付入りのファイル名(例:2026_07_10_000000_create_products_table.php)が新しく作成されます。

2.作成されたファイルを開きます。中身は以下のような形になっています。

public function up(): void
{
    Schema::create('products', function (Blueprint $table) {
        $table->id();
        $table->timestamps();
    });
}

3.$table->id();の下に、namepriceのカラムを追加します。

public function up(): void
{
    Schema::create('products', function (Blueprint $table) {
        $table->id();
        $table->string('name');
        $table->string('price');
        $table->timestamps();
    });
}

4.ターミナルで、以下を実行してマイグレーションを反映させます。

php artisan migrate

5.以下のようにDBの中にproductsを含むテーブルが作成されたら成功です。

4.シーディング(仮データ投入)

編集画面の動作確認をしやすくするため、productsテーブルにあらかじめ1〜2件のサンプルデータを入れておきます。

1.VS Codeのターミナルで、以下を実行し、Seederファイルを作成します。

php artisan make:seeder ProductSeeder

database/seedersフォルダの中に、ProductSeeder.phpというファイルが作成されます。

2.そのファイルを開き、run()メソッドの中身を以下のように書き換えます。

public function run(): void
{
    DB::table('products')->insert([
        ['name' => 'ボールペン', 'price' => '150'],
        ['name' => 'ノート', 'price' => '300'],
    ]);
}

3.保存(Ctrl+S)したら、ターミナルで以下を実行し、実際にデータを投入します。

php artisan db:seed --class=ProductSeeder

4.成功したら以下のようにデータが登録されます。

5.ルーティング作成

商品編集ページにアクセスするための「道」を作ります。Laravelでは、routes/web.phpというファイルに、URLとそれに対応する処理を書きます。

1.VS Codeでroutes/web.phpファイルを開きます。

2.ファイルの中身は、最初はこのようになっています。

<?php

use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    return view('welcome');
});

3.この下に、商品編集ページ用のルートを2つ追加します。1つは「表示用」、もう1つは「保存用」です。

<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\ProductController; // ★追加

Route::get('/', function () {
    return view('welcome');
});

Route::get('/products/{id}/edit', [ProductController::class, 'edit']); // ★追加
Route::post('/products/{id}/update', [ProductController::class, 'update']); // ★追加

4.保存(Ctrl+S)します。

この時点では、まだProductController自体が存在しないため、アクセスするとエラーになります。コントローラーの実装方法は後ほど解説します。

6. モデル作成

productsテーブルに対応するEloquentモデルを作成し、データの取得・更新処理をこちらに実装します。

1.VS Codeのターミナルで、以下を実行します。

php artisan make:model Product

app/Modelsフォルダの中に、Product.phpというファイルが作成されます。

2.作成されたファイルを開き、以下のように書き換えます。

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
	// 追加
    protected $fillable = ['name', 'price'];

	// 追加
	// 更新
    public function updateDetails(array $data)
    {
        return $this->update($data);
    }
}

$fillableは、一括代入(まとめて保存)を許可するカラムをLaravelに明示するための指定です。update()は渡されたデータで商品を更新するメソッドです。DBにアクセスする処理は、すべてこのモデルの中に閉じ込めています。

POINT

Eloquentは、Laravelに標準で組み込まれているORM(Object-Relational Mapping)ORMとは、DBのテーブルを、PHPのクラス(オブジェクト)として扱えるようにする仕組み。SQL文を直接書かなくても、PHPのメソッド呼び出しだけでDB操作ができる。ここでは、Productクラスが、まさにEloquentモデル。Modelクラスを継承するだけで、以下のようなメソッドが何も書かなくても自動的に使えるようになる

Product::findOrFail(1);    // idが1のデータを1件取得
Product::all();            // 全件取得
Product::where('price', '>', 100)->get();  // 価格が100より大きいものを取得
Product::create([...]);    // 新規作成
$product->update([...]);   // 更新
$product->delete();        // 削除

Eloquentには「命名規則による自動対応」という考え方がある。

  • クラス名Product(単数形) → テーブル名は自動的にproducts(複数形)だと推測される
  • 主キーは自動的にidカラムだと推測される

この規則に従っている限り、モデル側でテーブル名などを明示的に書く必要がない。

それもあり、モデルに独自メソッドを追加する際は、findupdateのようなEloquent標準メソッドと同じ名前を避けるのが無難です。技術的には標準メソッドを上書き(オーバーライド)することも可能ですが、Laravelの内部処理から意図せず呼び出されたり、他のエンジニアが標準の挙動だと思い込んでバグの原因になったりするリスクがあります。今回はupdateDetails()のように、標準メソッド名と衝突しない名前を使っています。

7.コントローラー作成

商品の「表示」と「更新」を実際に処理するコントローラーを作成します。

1.VS Codeのターミナルで、以下を実行します。

php artisan make:controller ProductController

app/Http/Controllersフォルダの中に、ProductController.phpというファイルが作成されます。

2.作成されたファイルを開き、中身を以下のように書き換えます。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Product;

class ProductController extends Controller
{

    // 追加
    // 編集画面を表示
    public function edit($id)
    {
        $product = Product::findOrFail($id);

        return view('products.edit', ['product' => $product]);
    }

    // 追加
    // 更新処理
    public function update(Request $request, $id)
    {
        // 入力チェック
        // validate()はバリデーション失敗時、ValidationExceptionを投げる。
        // この例外はLaravelのグローバル例外ハンドラーが自動でキャッチし、
        // 元のフォーム画面へリダイレクト+エラーメッセージ表示までを自動で行う。
        // そのため、失敗時は以下の行以降のコードは一切実行されない。
        // (catchや戻り値チェックが見当たらなくても、正常な挙動)
        $validated = $request->validate([
            'name' => 'required|string|max:255',
            'price' => 'required|numeric',
        ]);

        // DBからデータを取得
        $product = Product::findOrFail($id);

        // DBを更新
        $product->updateDetails($validated);

        return redirect('/products/' . $id . '/edit')->with('message', '更新しました');
    }
}

この2つのメソッドの役割

editメソッドは、URLの{id}部分(例:/products/1/editの「1」)を受け取り、該当する商品データをデータベースから探して、編集画面(products.editというBladeビュー、次のステップで作成)に渡します。

updateメソッドは、編集画面のフォームから送信されたnamepriceを受け取り、入力チェック後、該当する商品データを更新します。更新後は、もう一度編集画面に戻り、「更新しました」というメッセージを表示できるようにしています。

POINT

Laravelは、Controller・Model・Viewという基本構造は用意していますが、「複雑な業務ロジックをどこに書くか」「POSTされたデータを、型のある入れ物にどう詰め替えるか(DTO)」といった部分は、公式には規定していません。

これは、作者のTaylor Otwell氏が「シンプルさ」を重視する設計方針を貫いてきた結果とも言われていますが、実際の開発現場では、この「決まっていない部分」を埋めるために、Serviceクラスやspatie/laravel-dataのようなDTOパッケージが独自に生まれ、広く使われるようになっています。

「あれば使うが、標準にはないので、各プロジェクトが必要に迫られて自作している」というのが実態に近く、配属される現場によって設計方針が大きく異なる可能性がある、という点は覚えておくとよいでしょう。

3.保存(Ctrl+S)します。

この時点では、まだproducts.editというBladeビューが存在しないため、アクセスするとエラーになります。次の手順で作成します。

8.Bladeビュー作成

商品編集画面のHTML(Blade)を作成します。

1.VS Codeの左側エクスプローラーで、resources/viewsフォルダの中にproductsという名前の新しいフォルダを作成します。

2.そのproductsフォルダの中に、edit.blade.phpという名前のファイルを新規作成します。

3.作成したファイルに、以下の内容を書きます。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>商品編集</title>
</head>
<body>
    <h1>商品編集</h1>

    <!-- 追加 -->
    <!-- バリデーションエラーがあれば表示 -->
    @if ($errors->any())
        <ul style="color: red;">
            @foreach ($errors->all() as $error)
                <li>{{ $error }}</li>
            @endforeach
        </ul>
    @endif

    <!-- 追加 -->
    <!-- 更新成功メッセージがあれば表示 -->
    @if (session('message'))
        <p style="color: green;">{{ session('message') }}</p>
    @endif

    <form action="{{ url('/products/' . $product->id . '/update') }}" method="POST">
        @csrf

        <label>商品名</label><br>
        <input type="text" name="name" value="{{ old('name', $product->name) }}"><br>

        <label>価格</label><br>
        <input type="text" name="price" value="{{ old('price', $product->price) }}"><br>

        <button type="submit">保存</button>
    </form>
</body>
</html>

内容の補足

@if ($errors->any())は、バリデーションに失敗した際、エラー内容を一覧表示する部分です。@if (session('message'))は、更新成功時の「更新しました」というメッセージの表示部分です。

@csrfは、Laravelがフォーム送信時に必須で要求する、不正なリクエストを防ぐための仕組みです。これを忘れると、送信時にエラーになります。

{{ old('name', $product->name) }}は、入力欄に表示する値の指定です。バリデーションエラーで画面に戻ってきた場合は、直前にユーザーが入力していた値(old())を優先して表示し、それがなければ$product->name(DBに保存されている値)を表示します。これがないと、エラーで画面に戻った際、せっかく入力した内容が消えてしまいます。

4.保存(Ctrl+S)します。

5.以下のURLにアクセスしてみましょう。

http://localhost/demo_laravel_ts/public/products/1/edit

以下のような1件目の商品(ボールペン)の編集画面が表示されたら成功です。

⚠️ 「アクセスが拒否されました」が表示された場合は?

URLにアクセスすると以下のように「アクセスが拒否されました」というエラーが表示された場合、以下の手順を試してください。

1.エクスプローラーからstrageを右クリックしプロパティを選択します。

2.「読み取り専用」のチェックをはずしてOKを押します。

これはLaravel側の問題ではなく、Windowsのファイル書き込み権限に起因するエラー。Laravelはページ表示のたびに、storage/framework/viewsフォルダの中に一時ファイル(キャッシュ)を作ろうとするが、それが拒否されている状態。

6.更新も試してみましょう。

商品名や価格を書き換えて「保存」を押すと、「更新しました」というメッセージとともに、変更内容が反映されます。

試しに、価格を空欄にした状態で保存してみてください。先ほど追加したバリデーションが働き、赤い文字でエラーメッセージが表示されるはずです。

9.TypeScript導入

商品編集画面の中で、TypeScriptを使った処理を書けるようにします。

1.TypeScriptをインストールします。

VS Codeのターミナルで、以下を実行してください。

npm install --save-dev typescript

--save-dev:TypeScriptが開発時にだけ必要(本番公開時には不要)

2.TypeScriptの設定ファイルを作成します。

npx tsc --init

POINT

node、npm、npxの関係

Node.js(ランタイム本体)
└─ npm(パッケージ管理ツール、npm社が開発、Node.jsと同梱配布)
  └─ npx(パッケージ実行ツール、npmチームが開発、npmに同梱)

3.作成されたtsconfig.jsonを開き、以下のように編集します。

{
  // Visit https://aka.ms/tsconfig to read more about this file
  "compilerOptions": {
    // File Layout
    // "rootDir": "./src",
    // "outDir": "./dist",
    "outDir": "./public/js",     // 編集:コンパイル後のJSファイルの出力先
    "rootDir": "./resources/js", // 編集:コンパイル対象のTSファイルがある場所

    // Environment Settings
    // See also https://aka.ms/tsconfig/module
    "module": "esnext",     // 編集:ブラウザ向けのモジュール形式に変更(元はnodenext)
    "target": "es2020",     // 編集:ブラウザ向けのJSバージョンに変更(元はesnext)
    // "types": [],         // 編集:コメントアウト。DOM型を使うため無効化
    // For nodejs:
    // "lib": ["esnext"],
    "lib": ["es2020", "dom"], // 追加:document等、ブラウザ特有の型情報を追加
    // "types": ["node"],
    // and npm install -D @types/node

    // Other Outputs
    "sourceMap": false,      // 編集:デバッグ用の対応表ファイルを生成しない(元はtrue)
    "declaration": false,    // 編集:型定義ファイルを生成しない(元はtrue)
    "declarationMap": false, // 編集:型定義ファイルの対応表を生成しない(元はtrue)

    // Stricter Typechecking Options
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,

    // Style Options
    // "noImplicitReturns": true,
    // "noImplicitOverride": true,
    // "noUnusedLocals": true,
    // "noUnusedParameters": true,
    // "noFallthroughCasesInSwitch": true,
    // "noPropertyAccessFromIndexSignature": true,

    // Recommended Options
    "strict": true,
    // "jsx": "react-jsx",   // 編集:コメントアウト。Reactを使わないため不要
    "verbatimModuleSyntax": true,
    "isolatedModules": true,
    "noUncheckedSideEffectImports": true,
    "moduleDetection": "force",
    "skipLibCheck": true,
  },
  "include": ["resources/js/**/*"] // 追加:tscが監視・コンパイルする対象フォルダ
}

4.VS Codeのターミナルで、以下を実行します。

npx tsc --watch

product-edit.tsを保存するたびに、自動的にpublic/jsフォルダへ、コンパイル結果(product-edit.js)が書き出されるようになります。このコマンドは、実行したまま止まった状態になりますが、これは正常な動作です(ファイルを監視し続けているため)。終了したい場合はCtrl+Cを押してください。

POINT

POINT:なぜViteではなく、tsc(TypeScriptコンパイラ)を選んだか

LaravelでTypeScriptを扱う場合、多くの入門記事では、Laravel標準のビルドツールであるViteが案内されています。しかし、Viteの本領は、複数のJS・CSS・画像ファイルを、依存関係を解決しながら1つにまとめる(バンドリングする)ことにあります。

今回のような、独立したTypeScriptファイルを1つ追加するだけの構成では、Viteが持つ、この主要機能はメリットがうすれます。CSSの変換や、画像のキャッシュ対策といった機能も、今回は出番がありません。「Laravelプロジェクト作成時から、最初からViteが入っているから」という理由だけで採用すると、vite.config.js@vite()タグ、manifest.jsonといった、本来は不用だったViteでソースを複雑化させてしまうことにもなります。

そのため、今回はTypeScript標準のコンパイラであるtscを、直接使う方式を採用しています。vite.config.jsは不要で、Bladeには、普通の<script>タグを書くだけで済みます。

ちなみに、これから作る③(Laravel+Inertia+Vue)のような、複数のコンポーネントを組み合わせるSPA構成では、Viteのバンドリング機能が必須になります。Viteの実装方法はそちらの記事でご紹介します。

5.TypeScriptのソースファイルを作成します。

resources/jsフォルダの中に、product-edit.tsという名前のファイルを新規作成します。

6.中身は、次のセクションで実装していきます。まずは空のファイルとして、以下だけ書いておきます。

console.log('product-edit.ts loaded');

7.TypeScriptコンパイラがファイルの変更を検知し、public/jsフォルダの中に、自動的にJSファイルを作成します。

8.Bladeビュー(edit.blade.php)の</body>タグの直前に、以下を追加し、コンパイル後のJavaScriptファイルを読み込ませます。

    </form>
    <script src="{{ asset('js/product-edit.js') }}"></script>
</body>
</html>

asset('js/product-edit.js')は、publicフォルダの中にあるファイルへの、正しいURLを組み立てる、Laravel標準のヘルパー関数です。今回、tsconfig.jsonoutDir./public/jsに設定したため、コンパイル後のファイルはpublic/js/product-edit.jsに生成されます。この場所を指定しています。

9.ブラウザで、画面にアクセスします。

http://localhost/demo_laravel_ts/public/products/1/edit

10.ブラウザの開発者ツールを開きます。キーボードのF12を押すか、画面を右クリックして「検証」を選択します。

開発者ツールの「Console(コンソール)」タブを開き、以下のように表示されていれば、正しく読み込まれ動作しています。


これで、TypeScriptを扱う土台の準備が整いました。

10.TSでフォームの挙動を実装

今回は、保存ボタンを押した際、誤操作を防ぐため、確認ダイアログを表示するような実装を入れたいと思います。

1.resources/js/product-edit.tsを開き、以下のように書き換えます。

// 追加
// フォーム要素を取得
const form = document.querySelector('form');

// 追加
// 送信前に確認ダイアログを表示
form?.addEventListener('submit', (event) => {
    const isConfirmed = confirm('この内容で保存しますか?');

    if (!isConfirmed) {
        event.preventDefault();
    }
});

2.保存(Ctrl+S)します。

JSファイルが自動的に再ビルドされます。

11.動作確認

ブラウザを再読み込みし、「保存」ボタンを押してみてください。「この内容で保存しますか?」という確認ダイアログが表示され、「OK」を押した場合のみ、実際に更新処理が行われることを確認してください。

さいごに

今回は、Laravel+Blade+素のTypeScriptという、日本国内シェアNo.1の組み合わせで、商品編集ページを実際に構築してみました。

特に、「Laravelは自由度が高い分、正解が定まっていない部分が多い」という点は、実際に手を動かしてみて、初めて実感できたことだったと思います。配属される現場によって、設計方針が大きく異なる可能性がある、という点は、これからLaravelに触れる方には、ぜひ覚えておいてほしいポイントです。

次回は、②Express(TS)+EJSに進みます。同じproductsテーブル、同じ商品編集ページという題材で、今度はNode.js側から見た「通常のリロード型システム」を作っていきます。Laravel(PHP)との対比で、どんな違いが見えてくるか、引き続き検証していきます。