GASをclasp(CLIツール)+ TypeScriptでローカルで開発する

@Panda_Program

Google Apps ScriptをTypeScriptでローカルで開発する

Google Apps Script(以下、GAS)とはGoogleが開発したサーバレスな関数の実行環境です。GASはGoogleの各種サービスと連携してプログラムを実行できるため、業務やルーティンワークの自動化に最適です。

このブログでもGASを使ったハックを紹介してきました。

この記事では、claspというGoogle製のCLIツールを導入し、ローカル環境でTypeScriptを使ってGASを開発する方法をご紹介します。

まずclaspを導入し、TypeScriptを書いてGAS上でHello Worldします。次に、より実践的な使い方としてGmailをLINEに転送するコードをTypeScriptで実装します。

GAS用のCLIツールclaspを導入する

claspの導入方法は$ npm i @google/clasp -gを実行するだけです。

$ npm i @google/clasp -g

+ @google/[email protected]
added 160 packages from 92 contributors in 13.169s

これでclaspコマンドを実行できます。

$ clasp

clasp - The Apps Script CLI

Options:
  -v, --version
  -h, --help            output usage information

Commands:
  login [options]       Log in to script.google.com
  logout                Log out
  create [options]      Create a script
  ...

次にGoogleアカウントを使ってログインをします。コマンドは$ clasp loginです。

$ clasp login

Warning: You seem to already be logged in *globally*. You have a ~/.clasprc.json
Logging in globally...
🔑 Authorize clasp by visiting this url:
https://accounts.google.com/o/oauth2/v2/auth?access_type=xxx

Authorization successful.

Default credentials saved to: ~/.clasprc.json (/Users/Panda/.clasprc.json).

コマンドを実行すると新しくブラウザのタブが開きます。そこで、今回GASのプロジェクトを管理したいGoogleアカウントを選択しましょう。

ログインができたらタブを閉じてもOKです。

claspでTypeScriptを扱えるようにする

claspでTypeScriptを扱えるようにします。手順はclaspのGitHubを参考にしています。

まずはHello World用のプロジェクトを作成し、GASの型情報をインストールします。

$ mkdir hello-world
$ cd hello-world
$ npm i -S @types/google-apps-script
+ @types/[email protected]
added 1 package from 3 contributors and audited 1 package in 2.471s
found 0 vulnerabilities

次に、tsconfig.jsonを作成します。tsconfig.jsonは以下のように定義します。

tsconfig.json
{
  "compilerOptions": {
    "lib": ["es2019"],
    "experimentalDecorators": true
  }
}

GASはJavaScriptのランタイムエンジンV8をサポートしているので、ES2019の機能を使うことができます(但し、ES modulesは除きます)。

これでTypeScriptのGASプロジェクト作成の準備ができました。

GASのプロジェクトを作成する

では、claspでプロジェクトを作成してみましょう。$ clasp createコマンドを実行します。

$ clasp create --title "Hello World" --type standalone
\ Creating new script: Hello World...
Created new standalone script: https://script.google.com/d/XXXXXXXXXXXX/edit
Warning: files in subfolder are not accounted for unless you set a '.claspignore' file.
Cloned 1 file.
└─ appsscript.json

プロジェクトルートにappsscript.jsonが作成されました。デフォルトのTimeZoneはAmerica/NewYorkなので、Asia/Tokyoに変更します。

appsscript.json
{
  "timeZone": "Asia/Tokyo",
  "dependencies": {
  },
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8"
}

プロジェクトを作成できました。では、Hello Worldするコードを書いて、GASで実行してみましょう。

[基礎編]TypeScript + GASでHello Worldする

hello.tsを作成し、以下のように記述します。

hello.ts
const greeter = (person: string) => {
  return `Hello, ${person}!`;
}

function testGreeter() {
  const user = 'Panda 🐼';
  Logger.log(greeter(user));
}

GASでTypeScriptのコードを書いても動きません。しかし、claspがTypeScriptをJSにコンパイルしてくれるので、開発者はローカルでtsファイルを書くだけでいいのです。

このコードをGASにデプロイしてみましょう。コマンドは$ clasp pushです。

$ clasp push
└─ appsscript.json
└─ hello.ts
Pushed 2 files.

CLIでコマンドを実行すると、デプロイしたGASの画面を開くこともできます。$ clasp openを実行します。

$ clasp open
Opening script: https://script.google.com/d/XXXXXXXXXXXXXXXX/edit

ブラウザが立ち上がり、タブが開きました。

GASの画面

TypeScriptのコードはJavaScriptのコードにコンパイルされていることが確認できます。また、TypeScriptのバージョンが3.9.5であることもコメントから見て取れますね。

hello.js
// Compiled using ts2gas 3.6.2 (TypeScript 3.9.5)
var greeter = function (person) {
    return "Hello, " + person + "!";
};
function testGreeter() {
    var user = 'Panda 🐼';
    Logger.log(greeter(user));
}

testGreeterを実行して、ログを確認しましょう。

GASのログの画面

Hello, Panda 🐼!のと出力されています。testGreeterを実行できました。

[実践編]GmailをLINEに転送するGASを書く

次は、より実践的な例を紹介しましょう。

TypeScriptで記述していきます。「Gmailの新着メールをLINEに転送する by Google Apps Script」という記事で紹介したコードをTypeScript化していきます。

main.ts
const LINE_NOTIFY_TOKEN = PropertiesService
  .getScriptProperties()
  .getProperty('LINE_NOTIFY_TOKEN')
const ENDPOINT = 'https://notify-api.line.me/api/notify'

const FROM_ADDRESS = [''].join(' OR ')
const MINUTES_INTERVAL = 5

function main() {
  const notices = fetchNotices()

  if (notices.length === 0) {
    return
  }

  for (const notice of notices) {
    send(notice)
  }
}

function fetchNotices(): string[] {
  const now = Math.floor(new Date().getTime() / 1000)
  const intervalMinutesAgo = now - (60 * MINUTES_INTERVAL)
  const query = `(is:unread from:(${FROM_ADDRESS}) after:${intervalMinutesAgo})`

  const threads: GmailThread[] = GmailApp.search(query)

  if (threads.length === 0) {
    return []
  }

  const mails: GmailMessage[][] = GmailApp.getMessagesForThreads(threads)
  const notices: string[] = []

  for (const messages of mails) {
    const latestMessage: GmailMessage = messages.pop()
    const notice = `
--------------------------------------
件名: ${latestMessage.getSubject()}
受信日: ${latestMessage.getDate().toLocaleString()}
From: ${latestMessage.getFrom()}
--------------------------------------

${latestMessage.getPlainBody().slice(0, 350)}
`
    notices.push(notice)

    latestMessage.markRead()
  }

  return notices
}

function send(notice: string) {
  if (LINE_NOTIFY_TOKEN === null) {
    Logger.log('LINE_NOTIFY_TOKEN is not set.')
    return
  }

  const options: URLFetchRequestOptions = {
    'method': 'POST',
    'headers': {'Authorization': `Bearer ${LINE_NOTIFY_TOKEN}`},
    'payload': {'message': notice},
  }

  UrlFetchApp.fetch(ENDPOINT, options)
}

JSのコードと比較すると、getMessagesForThreadsの返り値が多次元配列になっていることが明確に意識できるのがとてもいいですね。

GASのGmailAppの型定義

また、型定義を確認するとgetPropertyでキーが存在しない場合はnullを返すことを知れたり、fetchのoptionsの型をURLFetchRequestOptionsに設定できました。

プログラムを型で縛ることは、実行時に発生するバグを未然に防ぐことに繋がります。

なお、今回参考にした型定義を抜粋は以下の通りです。

interface Properties {
  getProperty(key: string): string | null;
}

interface GmailApp {
  search(query: string): GmailThread[];
  getMessagesForThreads(threads: GmailThread[]): GmailMessage[][];
}

interface GmailMessage {
  getSubject(): string;
  getDate(): Base.Date;
  getFrom(): string;
  getPlainBody(): string;
  markRead(): GmailMessage;
}
interface UrlFetchApp {
  fetch(url: string, params: URLFetchRequestOptions): HTTPResponse;
}

interface HttpHeaders {
  [key: string]: string;
}
type HttpMethod = 'get' | 'delete' | 'patch' | 'post' | 'put';
type Payload = string | { [key: string]: any } | Base.Blob;

GASをローカルでTypeScriptで開発するメリット

GASをローカルで書くことができることは大きなメリットがあります。

  • GASに型注釈がつくことで、バグを生みにくくなる
  • 好きなエディタを使うことができる
  • 型定義にジャンプして、引数や返り値の型をチェックできる

特にIDEからショートカットキーを押すだけで型定義に飛べるので、返り値を確認するためにlogを吐き出したり公式ドキュメントを探す必要が無くなります。

このため、プログラムを組むスピードがTSがない場合と比べて爆速です。型定義はGASを書くスピードを上げるアクセルといっても過言ではないでしょう。

まとめ

コンパイラがあるプログラミング言語やTDDでもない限り、プログラムのデバッグ方法は「実行 → ログ確認 → 修正」が通常の流れです。

しかし、TypeScriptが型で縛ってくれるおかげで「実行 → ログ確認」のフィードバックループを回す回数は格段に減りました。

TypeScriptの素晴らしい開発体験をGASでも開発でも享受できるのは嬉しいことです。claspを開発し、TypeScriptに対応させてくれたGoogleのチームに感謝です。

Happy Coding 🎉

パンダのイラスト
パンダ

記事が面白いと思ったらツイートやはてブをお願いします!皆さんの感想が執筆のモチベーションになります。最後まで読んでくれてありがとう。

  • Share on Hatena
  • Share on Twitter
  • Share on Line
  • Copy to clipboard