TypeScript + Google Apps Scriptで作るスプレッドシート通知システムの実装

2026.01.025 min read
Loki

1. システム概要

プロジェクトの目的と要件

本プロジェクトは、Googleスプレッドシートで管理されているコンテンツ制作の進捗状況を監視し、特定の条件(期限間近、ステータス変更、コメント投稿など)に基づいてChatworkに自動通知を行うシステムです。 主な要件は以下の通りです。

  • 定期実行(毎日9時、12時)によるリマインド通知
  • 編集イベント(onEdit)によるリアルタイム通知
  • 複雑な条件判定(「投稿日の2日前かつ未完了」など)
  • メンテナンス性の高いコードベース

技術選定の理由

Google Apps Script (GAS)

Google Workspace(スプレッドシート)との親和性が最も高く、サーバーレスで手軽に定期実行やイベントトリガーを実装できるため採用しました。

TypeScript

GAS標準のJavaScriptではなくTypeScriptを採用することで、型安全性によるバグの抑制、IDE(VS Code)での補完機能による開発効率向上を実現しています。特にスプレッドシートの列定義やAPIレスポンスの型定義において威力を発揮します。

esbuild

TypeScriptで記述された複数のモジュールを、GASが解釈可能な1つのJavaScriptファイルにバンドルするために使用しています。高速なビルドと、依存関係の解決を担います。

Biome

コードのフォーマットとリンティングに使用。高速かつ設定不要でモダンなJavaScript/TypeScriptのベストプラクティスを強制できます。

2. アーキテクチャ設計

ファイル構成と責務分離

コードベースは機能ごとに適切に分割し、責務を明確にしています。

packages/[プロジェクト名]/src/
├── index.ts                  # エントリーポイント(GASから呼び出されるグローバル関数)
├── constant.ts               # 定数定義(列番号、メッセージテンプレートなど)
├── environment.ts            # 環境変数(スクリプトプロパティ)へのアクセス
├── chatwork.ts               # Chatwork API連携(commonパッケージのラッパー)
├── spreadsheet.ts            # スプレッドシートからのデータ取得・整形
├── message.ts                # 通知メッセージの生成ロジック
├── commentNotification.ts    # M列コメント通知のビジネスロジック
├── progressNotification.ts   # 週次進捗通知のビジネスロジック
├── postDateNotification.ts   # 投稿日関連(2日前、当日)のビジネスロジック
├── statusNotification.ts     # ステータスFIX通知のビジネスロジック
├── trigger.ts                # GASトリガーの作成・管理
└── test.ts                   # GAS上で実行するテスト関数群

関心の分離

  • データアクセス層: spreadsheet.ts がスプレッドシートの生データを扱い、型付きのオブジェクト(ContentRow)に変換して返します。
  • ビジネスロジック層: *Notification.ts が通知を行うべきかの判定ロジックを持ちます。
  • プレゼンテーション層: message.ts が通知文言の生成を担当します。
  • インフラ層: chatwork.ts が外部APIとの通信を担います。

再利用性 (Monorepo)

本プロジェクトはMonorepo構成を採用しており、packages/common に汎用的な処理(Chatwork APIの基本処理、日付操作など)を切り出しています。これにより、他のGASツールを作成する際にも共通コードを再利用可能です。

3. 実装のポイント

3.1 トリガーとイベント処理

GASのエントリーポイントとなる関数は、グローバルスコープに露出させる必要があります。index.ts で各機能をエクスポートし、GASのトリガーから呼び出せるようにしています。

コード例:トリガー作成関数 (src/trigger.ts)

/**
 * 毎日9時のトリガーを作成する
 * 実行関数: dailyAt9
 */
export const createDailyAt9Trigger = (): void => {
  ScriptApp.newTrigger("dailyAt9")
    .timeBased()
    .everyDays(1)
    .atHour(9)
    .create();
};
/**
 * onEditトリガーを作成する
 * 実行関数: onEditTrigger
 */
export const createOnEditTrigger = (): void => {
  ScriptApp.newTrigger("onEditTrigger")
    .forSpreadsheet(SpreadsheetApp.getActive())
    .onEdit()
    .create();
};

3.2 スプレッドシート操作

スプレッドシートの操作は実行速度のボトルネックになりやすいため、getValues() で一括取得してからメモリ上で処理を行うのが定石です。また、列番号を定数管理することで、シートレイアウト変更時の修正漏れを防ぎます。

コード例:データ抽出と型変換 (src/spreadsheet.ts)

// 行データをオブジェクトに変換
const parseContentRow = (row: unknown[], index: number): ContentRow | null => {
  const { HEADER_ROW_COUNT } = getEnvironment();
  // 列定義定数を使ってデータにアクセス
  const postDate = row[SHEET.POST_DATE_COL - 1];
  const content = row[SHEET.CONTENT_COL - 1];
  const status = row[SHEET.STATUS_COL - 1];
  return {
    rowIndex: index + HEADER_ROW_COUNT + 1,
    postDate: postDate instanceof Date ? postDate : null,
    content: String(content).trim() || "内容未設定",
    status: status ? String(status).trim() : "",
    // ...
  };
};

3.3 Chatwork API連携

APIトークンなどの機密情報はコードにハードコードせず、GASの「スクリプトプロパティ」で管理し、environment.ts 経由でアクセスします。

コード例:Chatwork APIラッパー (src/chatwork.ts)

/**
 * Chatworkにメッセージを投稿する
 */
export const postChatworkMessage = (roomId: string, body: string): boolean => {
  const { CHATWORK_API_TOKEN } = getEnvironment();
  if (!CHATWORK_API_TOKEN) {
    throw new Error("CHATWORK_API_TOKEN が設定されていません。");
  }
  // commonパッケージの関数を利用
  return postChatworkMessageCommon({
    apiToken: CHATWORK_API_TOKEN,
    roomId: roomId,
  }, body);
};

3.4 日付処理

「投稿予定日の2日前」などの判定では、タイムゾーン(JST)を考慮する必要があります。GASのデフォルトタイムゾーンとDateオブジェクトの扱いに注意が必要です。本プロジェクトでは、日付の正規化(時刻部分の切り捨て)を行った上で比較を行っています。

4. 開発環境とビルド

TypeScriptからGASへの変換

GASは標準ではES Modulesのインポート/エクスポート構文を理解しません(※V8ランタイムでもファイル間のimportは制限があります)。そのため、esbuild を使用してすべてのコードを単一のファイルにバンドルしています。

esbuild設定 (esbuild.config.js)

主な設定内容は以下の通りです。

  • bundle: true: 依存関係をすべて含める
  • format: 'iife': 即時実行関数式として出力(グローバル汚染を防ぐため、ただしGASのエントリーポイントは明示的にglobalに代入)
  • target: 'es2020': GASのV8ランタイムに合わせて設定

claspによる自動デプロイ

Google公式のCLIツール clasp を使用し、ローカルでビルドしたコードをコマンド一つでGASプロジェクトへプッシュします。

5. テスト戦略

GASはローカル環境での完全な実行シミュレーションが難しいため、以下の戦略をとっています。

  1. ロジックの単体テスト: 日付計算や文字列処理などはローカルのJest等でテスト可能(今回は簡易的に実装)。
  2. GAS上での結合テスト: test.ts にテスト用関数を実装し、GASエディタ上から手動実行して動作確認を行う。

コード例:テスト関数の実装 (src/test.ts)

/**
 * 毎日9時実行のテスト
 * 実際に通知が飛ぶので注意
 */
export const testDailyAt9 = (): void => {
  console.log("=== dailyAt9 テスト開始 ===");
  try {
    dailyAt9();
    console.log("=== dailyAt9 テスト終了: 成功 ===");
  } catch (e) {
    console.error(`=== dailyAt9 テスト終了: 失敗 ===\n${e}`);
  }
};

6. 運用とメンテナンス

スクリプトプロパティによる設定管理

通知先のChatworkルームIDやメンバーID、対象シート名などはすべてスクリプトプロパティで管理しています。これにより、担当者の変更やシートの変更があった場合でも、コードを修正することなく設定変更のみで対応可能です。

まとめ

本システムは、TypeScriptによる型安全性とモジュール化された設計により、拡張性と保守性の高いGASアプリケーションとなっています。単なる「スクリプト」の枠を超え、小さな「アプリケーション」として設計することで、長期的な運用に耐えうるシステムを実現しました。

End of Transmission