🦺

tRPCライクなGAS関数呼び出しライブラリを作ってみた

に公開

概要

GAS芸人の皆さん、こんにちは!

HtmlServiceを使ったWebアプリ開発、手軽に公開できて沼りますよね🫠
最近は@google/claspvite-plugin-singlefileを活用して、ReactやVueをGASにホストすることにハマっています。

補足:HtmlServiceとは?
Google Apps Script(GAS)でHTMLやJavaScriptを使ったWebアプリを作成・公開できる仕組みです。
GAS初心者の方は、公式ドキュメントも参考にしてみてください。

そんな中でフロントエンドからサーバサイド(GAS)の関数を呼び出すために必ず利用するAPIである
google.script.run」ですが、こんなお悩みありませんか?

  • 「サーバーサイド(GAS)の関数名や引数、なんだっけ...?🤔」
  • 「TypeScriptで書いてるのに、google.script.runだけ型が効かなくて不安...」
  • 「関数名を変えたら、フロント側で実行時エラーが出てしまった...😭」

そうですね、(global.)googleは型定義がないため、せっかくTypeScriptで開発しているのに、
一番重要な「関数の型定義」部分がany型になってしまいます。

そんな悩みを解決するために、サーバーサイド(GAS)の型定義を自動で抽出し、
フロントエンドで型安全に関数呼び出しできるようにするCLIツール「@ciderjs/gasnuki」を開発しました!

*npm
https://www.npmjs.com/package/@ciderjs/gasnuki

*Github repo
https://github.com/luthpg/gasnuki

この記事では、@ciderjs/gasnukiを使って、GAS開発の体験を向上させる方法をご紹介します

パッケージ名

賢明な読者諸君にはお分かりの通り、パッケージ名の「gasnuki」は「ガス抜き」と「GAS(から型定義を)抜き取る」をかけて付けました(オヤジギャグ)

開発のきっかけ・背景

GASをフロントエンドから呼び出す際、google.script.run.someFunction()のようなAPIが提供されていますが、
GASはJavaScriptベースの言語のため、型情報が一切提供されません

また、@types/google-apps-scriptパッケージはサーバー側(GAS本体)の型定義のみをカバーしており、

  • フロントエンドから呼び出す関数の型
  • google.scriptがグローバルに定義されること

など、実際の開発現場で必要となる型安全性や型補完を考慮していません。

このような背景から、GASとフロントエンド間で型情報を共有し、型安全な開発を実現したいという思いで
@ciderjs/gasnukiの開発をしてみました!

*最近、TanstackRouterに触れて「自動でルーティング型情報が抽出されるの、めっちゃええやんけ!」となったことも遠因です😊

GAS型抽出の使い方

インストール

npm install @ciderjs/gasnuki@latest

*CLI利用だけならdevDependenciesでよいのですが、後述のgoogle.script.runラッパーの利用のために--saveにしています

型定義の抽出

例えば、serverディレクトリ配下にGASのTypeScriptコードがある場合、以下のコマンドでtypesに型定義を抽出できます

npm gasnuki

利用可能なオプション

オプション 説明 デフォルト
-s, --srcDir <dir> ソースディレクトリ名(プロジェクトルートからの相対パス) "server"
-o, --outDir <dir> 出力ディレクトリ名(プロジェクトルートからの相対パス) "types"
-f, --outputFile <file> 出力ファイル名 "appsscript.ts"
-w, --watch 変更を監視して型定義を自動生成 (デフォルト: false)
-p, --project <project> プロジェクトのルートディレクトリパス プロジェクトルート
-v, --version バージョン番号を出力します -
-h, --help ヘルプを表示します -

フロントエンドでの型定義利用例

抽出された型定義ファイルにはグローバルにgoogleコンテキストを定義しています。
tsconfig.jsonでincludeしてあげれば、下記のようにgoogleについても型安全に利用できます。

フロントエンドでの呼び出しでも補完される!
google.script.run.
// エディタ補完が効き、`withSuccessHandler`などの組み込みメソッド群と、
// サーバー側で定義した関数群が表示される
tsconfig.jsonの設定例

型定義ファイル(例:types/appsscript.ts)がTypeScriptプロジェクトで認識されるよう、tsconfig.jsonincludeにディレクトリを追加してください。

{
  "compilerOptions": {
    // 諸設定群...
  },
  "include": ["src", "types"] // "types"ディレクトリを認識させる
}

ウォッチオプション(npx gasnuki -w)で裏で起動しておけば、
サーバサイドの変更にも常時対応し、フロントエンドとの型情報不一致もなくなることでしょう…!

また、抽出された型定義(例:types/appsscript.ts)をフロントエンドのTypeScriptプロジェクトでimportすることで、
GAS関数の型安全な呼び出しが可能になります

フロントエンドでの利用例
// types/appsscript.ts で定義された型を利用
import type { ServerScripts } from '../types/appsscript';

// google.script.run で型安全に呼び出し
const callGas = (...args: Parameters<ServerScripts['someFunction']>) => {
  google
    .script
    .run
    .withSuccessHandler(
      // withSuccessHandler内のコールバックまでは型推論されないため
      (result: ReturnType<ServerScripts['someFunction']>) => console.log(result)
    )
    .someFunction(...args);
};

tRPCライクなgoogle.script.run呼び出し

さて、型定義だけを抽出しても面白みがないので、通常はコールバックスタイルしか提供しないgoogle.script.runをPromise化させ、
tRPCのような型安全なPromiseでの使い方をご紹介します

Promise化は以下記事を参考にしています:

https://zenn.dev/37cohina/articles/dc88cefac79ab1

https://qiita.com/Shankou/items/7b73686c3aa9364c20ce

./src/modules/server.ts
import type { ServerScripts } from "../../types/appsscript";
import { getPromisedServerScripts, type PartialScriptType } from "@ciderjs/gasnuki/promise";

type Mockup = PartialScriptType<ServerScripts>;

const sleep = async (sec: number) => await new Promise(resolve => setTimeout(resolve, 1000 * sec));

// ローカル開発時にGASサーバサイド処理をモックアップするための代替処理
const mockupFunctions: Mockup = {
  async getTableData(key) { // モックアップにも抽出済のGAS型定義が適用されるので、引数`key`の型も推論される
    await sleep(1); // GASとの通信を模して1秒の待機時間を作る
    return JSON.stringify([
      ['No', '名前', 'アドレス', 'ラベル'],
      [1, 'テスト1', 'user01@example.com', key],
      [2, 'テスト2', 'user02@example.com', key],
    ]); // GASが返却するデータはJSON化する必要がある
  },
};

export const serverScripts = getPromisedServerScripts<ServerScripts>(mockupFunctions);

このserverScriptsには、@ciderjs/gasnukiで抽出した型定義に則り、かつPromise化された以下関数が紐づいています

  • GAS環境:実際のGAS関数群
  • ローカル環境:mockupFunctionsで定義したモック処理群
利用例 ./src/index.ts
import { serverScripts } from './modules/server';
import type { Content } from '../types/appsscript'; // サーバサイドファイルで定義した`Interface`情報もエクスポートしている

const getServerTableData = async (): Promise<Content[] | null> => {
  try {
    const response = await serverScripts.getTableData('label'); // Promise化された関数も型情報とともに補完が効く
    return JSON.parse(response) as Content[];
  } catch (e) {
    console.error(e);
    return null;
  }
}

まとめ

GAS開発であろうとも、工夫すればtRPCライクに型安全性を高め、より快適な開発体験を実現できます
@ciderjs/gasnukiを使って、サーバーサイドとフロントエンドの間で型情報を共有し、
TypeScriptの恩恵を最大限に活かしましょう!

型安全開発で、より良いGASライフを!🚀

Discussion