tRPCライクなGAS関数呼び出しライブラリを作ってみた
概要
GAS芸人の皆さん、こんにちは!
HtmlService
を使ったWebアプリ開発、手軽に公開できて沼りますよね🫠
最近は@google/clasp
とvite-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
*Github repo
この記事では、@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.json
のinclude
にディレクトリを追加してください。
{
"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化は以下記事を参考にしています:
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
で定義したモック処理群
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