clasp が Typescript をサポートした！

以前、[GAS のGoogle謹製CLIツール clasp](https://qiita.com/HeRo/items/4e65dcc82783b2766c03)と題して、[@google/clasp](https://github.com/google/clasp)を紹介した。

このツールと新しい [Google Apps Scriptの管理コンソール](https://script.google.com/home)と [Stackdriver Loggingのサポート](https://qiita.com/HeRo/items/946dff8c9161a730ec66)により GASの開発、運用環境は格段に使いやすくなった。

その後も開発は進み、なんと v1.5.0 以降で Typescript をサポートしたので、試してみた。

## インストール

次のコマンドで、プロジェクトを初期化する[^1]。

```bash
$ 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 は必須ではないがグッドマナーとして導入しておこう
```

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

Typescriptは明示的にインストールしなくても*@google/clasp*が依存しているのでインストールされる。
*@types/google-apps-script* によって VSCode等では**コード補完**ができるようになる。素晴らしい！
しっかり、`SpreadsheetApp`など、**GAS固有のクラス群も定義されている**。素晴らしい！！

## GAS スクリプトの作成

次のコマンドで、GASのファイルをGoogle Driveに作成して、生成されたコードをローカルにpullしている[^2]。

[^2]: これまで、`clasp`を使ったことがなければ、ログインと、APIの有効化が必要になるが、[GAS のGoogle謹製CLIツール clasp](https://qiita.com/HeRo/items/4e65dcc82783b2766c03)を参考にしてほしい。

```bash

$ clasp create clasp-ts-sample --rootDir ./src
? Clone which script?  (Use arrow keys)
❯ standalone
  docs
  sheets
  slides
  forms
  webapp
  api
```

`--rootDir`オプションはつけることをおすすめする。
というのも、`--rootDir`オプション付けずに作成すると、`clasp push`を実行したときに、*node_modules*以下のすべてのJSを読み込もうとして、失敗する。
なお、`--rootDir`オプションはclasp 1.6.0で追加されたのでclaspが古い場合にはアップデートする。

また、clasp 1.7.0からは作成するGASプロジェクトのタイプを選択できるようになった。
`standalone`や`webapp`、`api`を選択すると GASのスクリプトだけ作成されるが、`docs`や`sheets`を選択すると対応するコンテナドキュメントも一緒に作られる。

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

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

## `--rootDir`オプションを付けずに作成してしまった場合

うっかり忘れたとか、clasp 1.5.x以下で作成した場合は次の様に*.clasp.json* に `rootDir`を追記する。

```json:.clasp.json
{
  "scriptId":"******-***************************************************",
  "rootDir": "./src"
}
```

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

```bash
$ mkdir src
$ mv appsscript.json src/
```

これで`--rootDir`オプションを使った場合と同じ状態となる。

## TypescriptのコードをPUSHしてみる。

claspのリポジトリにある[サンプル](https://github.com/google/clasp/blob/master/docs/typescript.md)をコピーして試してみる。
それが次のコード。alert を使っていた部分は、GASでは動かないので修正している。

```typescript:Code.ts
// Optional Types
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");

// Classes
class Hamburger {
  constructor() {
    // This is the constructor.
  }
  public listToppings() {
    // This is a method.
  }
}

// Template strings
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)`

// Spread operator (array)
const cde = ["c", "d", "e"];
const scale = ["a", "b", ...cde, "f", "g"];  // ['a', 'b', 'c', 'd', 'e', 'f', 'g']

// Spread operator (map)
const mapABC  = { a: 5, b: 6, c: 3};
const mapABCD = { ...mapABC, d: 7};  // { a: 5, b: 6, c: 3, d: 7 }

// Destructure map
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 Services
const doc = DocumentApp.create("Hello, world!");
doc.getBody().appendParagraph("This document was created by Google Apps Script.");

// Decorators
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'
```

`tsc`などを使って**事前にトランスパイルする必要はない**。
次のコマンドだけで、**自動的にトランスパイル**して、GASにPUSHしてくれる。
*tsconfig.json*すら**用意する必要がない**。
JSのコードをPUSHするようにTSのコードをPUSHできるのだ。
もう、JSで、GASを実装する理由が見当たらない。

```
$ clasp push
```
GASにPUSHされたコードを見てみる。次の様にトランスパイルされている。

```javascript: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;
};

// Optional Types
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() {
        // This is the constructor.
    }
    Hamburger.prototype.listToppings = function () {
        // This is a method.
    };
    return Hamburger;
}());
// Template strings
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)`
// Spread operator (array)
var cde = ["c", "d", "e"];
var scale = ["a", "b"].concat(cde, ["f", "g"]); // ['a', 'b', 'c', 'd', 'e', 'f', 'g']
// Spread operator (map)
var mapABC = { a: 5, b: 6, c: 3 };
var mapABCD = __assign({}, mapABC, { d: 7 }); // { a: 5, b: 6, c: 3, d: 7 }
// Destructure map
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 Services
var doc = DocumentApp.create("Hello, world!");
doc.getBody().appendParagraph("This document was created by Google Apps Script.");
// Decorators
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'
```

## 動作確認
試しに*Override*を実行してみる。
*Override*以外の関数は実行されないが、関数外の部分は実行される。
もちろんちゃんと動く。

`console.log`の出力は*Stackdriver Logging*に次のように出力される。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/32533/5baf337b-572a-9519-84e6-0eec06c907e3.png)

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

![image.png](https://qiita-image-store.s3.amazonaws.com/0/32533/5a99787d-a04c-47be-2d90-063cf28ca364.png)

おお、感動的に簡単。
これまで、WebpackやBabelを使ってGAS用にトランスパイルしていたが、それらがバカバカしくなるほどだ。
どんどんバージョンアップするWebpackやBabelに追従しようとしてアップデートするとビルドできなくなるようなトラブルからも解放される。

しかも、`clasp push`には*watch*モードまである。
次のコマンドを実行しておけば、コードの変更を検知すると再PUSHしてくれる。

```bash
$ clasp push --watch
```

## おまけ： VSCode の設定

次の設定を VSCode のUser Settings に追加しておくと、*.clasp.json* や *appsscript.json*でもコード補完できるようになる。

```json
{
    "json.schemas": [{
        "fileMatch": [ "appsscript.json" ],
        "url": "http://json.schemastore.org/appsscript"
    },
    {
        "fileMatch": [ ".clasp.json" ],
        "url": "http://json.schemastore.org/clasp"
    }]
}
```

## まとめ
- *@google/clasp* が Typescriptをサポートし、一層使えるツールになった。
- `clasp push` で 自動的にトランスパイルしてPUSHしてくれる
- クラスはもちろん、テンプレート文字列やスプレッド構文などモダンなコードでGASを実装できる

## 参考
- [@google/clasp/typescript](https://github.com/google/clasp/blob/master/docs/typescript.md)