Google Apps ScriptsでTypescriptが超簡単に使えるようになった! - アクトインディ開発者ブログ

<a class="keyword" href="http://d.hatena.ne.jp/keyword/Google%20Apps">Google Apps</a> ScriptsでTypescriptが超簡単に使えるようになった! - アクトインディ開発者ブログ


Google Apps ScriptsでTypescriptが超簡単に使えるようになった!


morishitaです。

Cloud Functions と並ぶ(?)Google のサーバレスな JavaScript 実行環境といえば Google Apps Scripts(GAS)です。
GAS ってあの Excel で言う VB スクリプト環境のようなものでしょう? と思ったあなた!

このエントリでその認識が変わると思います。

以前は使いやすいとは言い難かったGASですが、最近は使いやすくなってきました。
といっても、GAS 自体がアップデートされたのではなく周辺ツールが整備が進み開発・運用しやすい状況が整ってきたからです。

そして、なんと最近Typescript でとても実装しやすくなったので、それをご紹介したいと思います。

google/clasp

以前の GAS は Web エディタ上でしか実装できず、コードを VCS で管理することもままならない状況でしたが、Google からgoogle/claspがリリースされ、状況が改善されました。

これは GAS を管理するための CLI ツールで、Google Drive 上の GAS のコードをローカルに pull したり、逆に Google Drive 上の GAS のプロジェクトに push したりできます。

ということは、Git で管理しながらローカルの使い慣れたエディタでコードを書いて、GAS に push して実行するという開発ができるのです1

google/clasp が、この平成最後の夏にリリースされた v1.5.0 でなんと Typescript をサポートしたのです。

これまでも Webpack や Babel を使ってトランスパイルして ES6 や Typescript で GAS の開発はできました。
しかし、どんどんバージョンアップする Webpack や Babel に追従しようとしてアップデートするとビルドできなくなるようなトラブルも起こりがちでした。

でも、その苦労から解放されたのです。

少々、前置きが長くなりましたが、実際に使ってみましょう。

@google/clasp のインストールとローカル環境の初期化

google/clasp は Node.js のモジュールです。Node.js 4.7.4 以上が必要なので、用意してください。

Node.jsの準備ができたら、次のコマンドで、ローカル環境を作ります2

$ mkdir clasp-ts-sample
$ cd clasp-ts-sample
$ npm init -y
$ npm install @google/clasp tslint -D
$ npm install @types/google-apps-script -S
$ tslint --init # tslint は必須ではありませんが、大人のたしなみとして導入しましょう。


Typescript は明示的にインストールしなくても@google/claspが依存しているのでインストールされます。2018/09/10時点では Typescript 2.9.2がインストールされます。

@types/google-apps-script も導入することにより VSCode 等ではコード補完されるようになります。

SpreadsheetAppなど、GAS 固有のクラス群も定義されています

素晴らしい!!

GAS プロジェクトの作成

次のコマンドで、GAS プロジェクトのファイルを Google Drive に作成します。その後、生成されたコードをローカルに pull します3

$ clasp create clasp-ts-sample
$ clasp pull


ここまででできたファイル構成は次の通りです。

clasp-ts-sample/
├── .clasp.json
├── node_modules/
├── package-lock.json
├── package.json
├── Code.js
├── appsscript.json
└── tslint.json


rootDirを設定し、ソースファイルを src に移動する

実装を開始する前に、環境準備にもうひと手間かけます。

というのも、このまま、 clasp pushを実行すると、node_modules以下のすべての JS を読み込もうとして失敗します。

.claspignore を作って無視してやることもできますが、オススメは .clasp.jsonrootDirを定義する方法です。

次の様に.clasp.jsonrootDirを追加します。

{
"scriptId": "******-***************************************************",
"rootDir": "src"
}


そして、src ディレクトリを作って、clasp pushの対象となるファイルを移動します。

$ mkdir src
$ mv appsscript.json src/
$ mv Code.js src/Code.ts


これで準備は終了。次のようなファイル構成になります。

clasp-ts-sample/
├── .clasp.json
├── node_modules/
├── package-lock.json
├── package.json
├── src/
│ ├── Code.ts
│ └── appsscript.json
└── tslint.json


Typescript のコードを PUSH してみる

clasp のリポジトリにあるサンプルをコピーして試してみます。

それが次のCode.ts です。alert を使っていた部分は、GAS では動かないので修正しています。

// 型定義
const isDone: boolean = false;
const height: number = 6;
const bob: string = "bob";
const list1: number[] = [1, 2, 3];
const list2: number[] = [1, 2, 3];

enum Color {
Red,
Green,
Blue
}

const c: Color = Color.Green;
let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false; // okay, definitely a boolean

function showMessage(data: string): void {
// Void
Logger.log(data);
}
showMessage("hello");

// クラス
class Hamburger {
constructor() {
// コンストラクタ
}
public listToppings() {
// メソッド
}
}

// テンプレート文字列
const name = "Sam";
const age = 42;
console.log(`hello my name is ${name}, and I am ${age} years old`);

// Rest arguments
const add = (a: number, b: number) => a + b;
const args = [3, 5];
add(...args); // same as `add(args[0], args[1])`, or `add.apply(null, args)`

// スプレッド構文 (array)
const cde = ["c", "d", "e"];
const scale = ["a", "b", ...cde, "f", "g"]; // ['a', 'b', 'c', 'd', 'e', 'f', 'g']

// スプレッド構文 (map)
const mapABC = { a: 5, b: 6, c: 3 };
const mapABCD = { ...mapABC, d: 7 }; // { a: 5, b: 6, c: 3, d: 7 }

// 分割代入
const jane = { firstName: "Jane", lastName: "Doe" };
const john = { firstName: "John", lastName: "Doe", middleName: "Smith" };
function sayName({ firstName, lastName, middleName = "N/A" }) {
console.log(`Hello ${firstName} ${middleName} ${lastName}`);
}
sayName(jane); // -> Hello Jane N/A Doe
sayName(john); // -> Helo John Smith Doe

// Export (The export keyword is ignored)
export const pi = 3.141592;

// Google Apps Script の独自サービスの利用
const doc = DocumentApp.create("Hello, world!");
doc
.getBody()
.appendParagraph("This document was created by Google Apps Script.");

// デコレータ(高階関数)
function Override(label: string) {
return (target: any, key: string) => {
Object.defineProperty(target, key, {
configurable: false,
get: () => label
});
};
}
class Test {
@Override("test") // invokes Override, which returns the decorator
public name: string = "pat";
}
const t = new Test();
console.log(t.name); // 'test'


どうでしょう、次のような Typescript ならではのものを含むモダンな実装を含んでいます。

  • 型アノテーション
  • クラス
  • テンプレート文字列
  • スプレッドオペレータ
  • 部分代入


そして、Google Docs を扱う DocumentAppを利用するコードも含んでいます。

では、Google Drive 上の GAS プロジェクトに push してみましょう。

次のコマンドだけで、自動的にトランスパイルして、GAS に push してくれます。

$ clasp push


tscなどを使って事前にトランスパイルする必要はありません

tsconfig.jsonすら用意不要です4

Javascript のコードを push するように Typescript のコードも push できます。

続いて GAS プロジェクトに push されたコードを見てみましょう。
clasp open コマンドを実行すると Google Drive 上の GAS プロジェクトがブラウザで開きます。

次の様にファイル Code.gs としてトランスパイルされています。

var exports = exports || {};
var module = module || { exports: exports };
var __assign =
(this && this.__assign) ||
Object.assign ||
function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s)
if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
}
return t;
};
var __decorate =
(this && this.__decorate) ||
function(decorators, target, key, desc) {
var c = arguments.length,
r =
c < 3
? target
: desc === null
? (desc = Object.getOwnPropertyDescriptor(target, key))
: desc,
d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function")
r = Reflect.decorate(decorators, target, key, desc);
else
for (var i = decorators.length - 1; i >= 0; i--)
if ((d = decorators[i]))
r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};

// 型定義
var isDone = false;
var height = 6;
var bob = "bob";
var list1 = [1, 2, 3];
var list2 = [1, 2, 3];
var Color;
(function(Color) {
Color[(Color["Red"] = 0)] = "Red";
Color[(Color["Green"] = 1)] = "Green";
Color[(Color["Blue"] = 2)] = "Blue";
})(Color || (Color = {}));
var c = Color.Green;
var notSure = 4;
notSure = "maybe a string instead";
notSure = false; // okay, definitely a boolean
function showMessage(data) {
Logger.log(data);
}
showMessage("hello");
// Classes
var Hamburger = /** @class */ (function() {
function Hamburger() {
// コンストラクタ
}
Hamburger.prototype.listToppings = function() {
// メソッド
};
return Hamburger;
})();
// テンプレート文字列
var name = "Sam";
var age = 42;
console.log("hello my name is " + name + ", and I am " + age + " years old");
// Rest arguments
var add = function(a, b) {
return a + b;
};
var args = [3, 5];
add.apply(void 0, args); // same as `add(args[0], args[1])`, or `add.apply(null, args)`
// スプレッド構文 (array)
var cde = ["c", "d", "e"];
var scale = ["a", "b"].concat(cde, ["f", "g"]); // ['a', 'b', 'c', 'd', 'e', 'f', 'g']
// スプレッド構文 (map)
var mapABC = { a: 5, b: 6, c: 3 };
var mapABCD = __assign({}, mapABC, { d: 7 }); // { a: 5, b: 6, c: 3, d: 7 }
// 部分代入
var jane = { firstName: "Jane", lastName: "Doe" };
var john = { firstName: "John", lastName: "Doe", middleName: "Smith" };
function sayName(_a) {
var firstName = _a.firstName,
lastName = _a.lastName,
_b = _a.middleName,
middleName = _b === void 0 ? "N/A" : _b;
console.log("Hello " + firstName + " " + middleName + " " + lastName);
}
sayName(jane); // -> Hello Jane N/A Doe
sayName(john); // -> Helo John Smith Doe
// Export (The export keyword is ignored)
exports.pi = 3.141592;
// Google Apps Script の独自サービスの利用
var doc = DocumentApp.create("Hello, world!");
doc
.getBody()
.appendParagraph("This document was created by Google Apps Script.");
// デコレータ(高階関数)
function Override(label) {
return function(target, key) {
Object.defineProperty(target, key, {
configurable: false,
get: function() {
return label;
}
});
};
}
var Test = /** @class */ (function() {
function Test() {
this.name = "pat";
}
__decorate(
[
Override("test") // invokes Override, which returns the decorator
],
Test.prototype,
"name"
);
return Test;
})();
var t = new Test();
console.log(t.name); // 'test'


動作確認

GAS の Web エディターでは 3 つの関数が実行対象として選択できると思います。
その中から試しにOverrideを実行してみます。
Override以外の関数は実行されませんが、関数外の部分は実行されます。
もちろんちゃんと動きます。

console.logの出力はStackdriver Loggingに次のように出力されます。
f:id:HeRo:20180908100232p:plain

また、DocumentApp.createして、中に文字列を書き込んでいる部分がありますが、
その出力として次のようなGoogle Docのファイルが Google Driveの中に作成されます。

f:id:HeRo:20180908100329p:plain

とても簡単です。

また、clasp pushにはwatchモードまであります。
次のコマンドを実行しておけば、コードの変更を検知すると自動的に再 push してくれます。

$ clasp push --watch


これで実装->実行->また実装 のサイクルが少し楽になりますね。

まとめ

どうでしょう、これまで GAS を使ってきた方には、今までのやり方がバカバカしくなるほど簡単に Typescript で実装できることがおわかりいただけたと思います。

もう Typescript で GAS を実装しない理由が見当らないでしょう?

GAS は Cloud Functions に比べると制約が多く、Google Drive 上のアプリケーションの拡張用と思われがちです。

しかし次のような特徴を備えており、ユースケース次第では大変便利に使えるサービスです。

  • Sheets や Docs、Slides といった Google Drive 上のアプリケーションにアクセスしやすい
  • Gmail、BigQuery や Analytics などの一部の Google のサービスを利用でき、しかも SDK よりも手軽に使えるものもある
  • Web アプリケーションも作れる
  • 定期実行可能
  • そして、無料5


特に、BigQuery や Analytics のデータを集計して、レポートを作成する作業を自動化するには最も便利な環境だと思います。
SheetsやSlidesのファイルとしてGoogle Drive上に出力するのが簡単ですし、Gmail経由でメールも出せますし、定期実行できますし。

また、去年次の 2 つが使えるようになり、ますます運用しやすくなりました。

  • Apps Script dashboard

    • GAS 専用の管理ダッシュボード。
    • Google Drive に散らかりがちな GAS プロジェクトを一元管理できます。
    • Sheets ファイルなどに含まれる container-bound な GAS プロジェクトも管理できます。
  • Stackdriver Logging

    • 汎用のロギングサービス。
    • console.log等の出力がログとして記録されます。
    • デバッグや実行状況の確認が格段にやりやすくなりました。


うまく使えば業務の効率化に大いに役立ってくれる GAS を Typescript でモダンに開発しましょう。

参考


最後に

アクトインディでは エンジニアを募集しています。

actindi.net





  1. このような開発スタイルを最初に実現し、エポックメイキングなツールだったnode-google-apps-scriptはすでにディスコンとなっています。


  2. claspnpm install -g @google/claspでグローバルにインストールしてもいいのですが、私は ndenv で複数バージョンの Node.js をインストールしており、プロジェクトごとに Node.js のバージョンが異なったりします。それで、グローバルなインストールは避けています。代わりに、./node_module/.binを PATH に追加してプロジェクトディレクトリにインストールしたコマンドを実行できるようにしています。


  3. これまで、claspを使ったことがなければ、ログインと、API の有効化が必要になります。参考:GAS の Google 謹製 CLI ツール clasp


  4. claspts2gasを利用してトランスパイルしています。コンパイルオプションはこちら =>compilerOptions


  5. GAS のスクリプトの実行自体は無料ですが、有料サービスの API 呼び出た場合、別途課金されます。