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
」を開発しました!
この記事では、@ciderjs/gasnuki
を使って、GAS開発の体験を向上させる方法をご紹介します
パッケージ名
賢明な読者諸君にはお分かりの通り、パッケージ名の「gasnuki」は「ガス抜き」と「GAS(から型定義を)抜き取る」をかけて付けました(オヤジギャグ)
開発のきっかけ・背景
GASをフロントエンドから呼び出す際、google.script.run.someFunction()
のようなAPIが提供されていますが、
GASはJavaScriptベースの言語のため、型情報が一切提供されません
また、@types/google-apps-script
パッケージはサーバー側(GAS本体)の型定義のみをカバーしており、
- フロントエンドから呼び出す関数の型
-
google.script
がグローバルに定義されること
など、実際の開発現場で必要となる型安全性や型補完を考慮していません。
このような背景から、GASとフロントエンド間で型情報を共有し、型安全な開発を実現したいという思いで
@ciderjs/gasnuki
の開発をしてみました!
*最近、TanstackRouterに触れて「自動でルーティング型情報が抽出されるの、めっちゃええやんけ!」となったことも遠因です😊
使い方
インストール
npm install --save-dev @ciderjs/gasnuki
型定義の抽出
例えば、server
ディレクトリ配下にGASのTypeScriptコードがある場合、以下のコマンドでtypes
に型定義を抽出できます
npx 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
やtypeRoots
にtypes
ディレクトリを追加してください。
{
"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);
};
Promise
化しても型定義がほしい!
さて、型定義だけを抽出しても面白みがないので、Promise化させた際の型安全な使い方をご紹介します
Promise化は以下記事を参考にしています:
import type { ServerScripts } from "../types/appsscript";
type Promised<T> = Readonly<{
[K in keyof T]: T[K] extends (...args: infer A) => infer R
? (...args: A) => Promise<R>
: T[K];
}>;
export const PromisedServerScripts = new Proxy({} as Promised<ServerScripts>, {
get: (_, method: keyof ServerScripts) => {
type TargetScriptType = ServerScripts[typeof method];
if (
['withSuccessHandler', 'withFailureHandler', 'withUserObject'].includes(
String(method),
)
) {
throw new Error('Method not allowed');
}
return (...args: Parameters<TargetScriptType>) =>
new Promise<ReturnType<TargetScriptType>>((resolve, reject) => {
google.script.run
.withSuccessHandler<ReturnType<TargetScriptType>>(resolve)
.withFailureHandler(reject)
// @ts-expect-error arguments has some types
[method](...args);
});
},
set: () => {
throw new Error('Method not allowed');
}
});
Promise化させた関数をProxyで返すようにしています。
その中で@ciderjs/gasnuki
で生成されたサーバサイドの型定義も読み込むことで、型安全性を確保しています。
import { PromisedServerScripts } from '@/modules/appsscript-promise';
import type { Content } from '../types/appsscript'; // サーバサイドファイルで定義したタイプ情報もエクスポートしている
const callGas = async () => {
try {
const response = await ServerScripts.getContents('label'); // Promise化された関数も型情報とともに補完が効く
return JSON.parse(response) as Content;
} catch (e) {
console.error(e);
}
}
まとめ
GAS開発だって工夫すれば型安全性を高め、より快適な開発体験を実現できます
@ciderjs/gasnuki
を使って、サーバーサイドとフロントエンドの間で型情報を共有し、
TypeScriptの恩恵を最大限に活かしましょう!
型安全開発で、より良いGASライフを!🚀
Discussion