GASのWebクライアントからTypeScript(型安全)でサーバサイドの関数を呼びたい

# GASのWebクライアントから型安全でサーバサイドの関数を呼びたい

TypeScriptでGASの開発をする場合、GASが提供するAPIの型定義は[@types/google-apps-script](https://www.npmjs.com/package/@types/google-apps-script)を使えば型安全でコーディングができます。GASでWebアプリではクライアントサイドからGASの関数を[google.script.run](https://developers.google.com/apps-script/guides/html/reference/run)を使って簡単に呼び出せますが、こちらの型定義は提供されていません。
折角、サーバサイドはTypeScriptで書いているのであればクライアントサイドでその型定義を利用したいので、クライアントサイドの型定義(d.ts)を生成するようにしてみました。

## 前提

- GASのモダン開発をしている(TypeScriptでやっている)
- クライアントサイドのコードにもビルドプロセス(Webpack等)を適用している。

## クライアントサイドの型定義

[Definitely Typed](https://github.com/DefinitelyTyped/DefinitelyTyped) では提供されていませんが、個人で作成されたものが [Gist
](https://gist.github.com/DominikPalo/bea3d958877566fe24ebe5ee1688d976) にUPされています。

この Gist に UP されてる d.ts を見てみると `google.script.run` で呼び出す関数の定義は以下のように `[serverSideFunction: string]: Function;` と定義されています。

```ts
namespace google {
    namespace script {
        interface IRun {
            [serverSideFunction: string]: Function;
            /**
             * Sets a callback function to run if the server-side function throws an exception. Without a failure handler, failures are logged to the JavaScript console. To override this, call withFailureHandler(null) or supply a failure handler that does nothing.
             * @param callback a client-side callback function to run if the server-side function throws an exception; the Error object is passed to the function as the first argument, and the user object (if any) is passed as a second argument
             */
            withFailureHandler(callback: (error: Error, object?: any)=>void): IRun;
            /**
             * Sets a callback function to run if the server-side function returns successfully.
             * @param callback a client-side callback function to run if the server-side function returns successfully; the server's return value is passed to the function as the first argument, and the user object (if any) is passed as a second argument
             */
            withSuccessHandler(callback: (value: any, object?: any)=>void): IRun;
            /**
             * Sets an object to pass as a second parameter to the success and failure handlers.
             * @param {Object} object an object to pass as a second parameter to the success and failure handlers; because user objects are not sent to the server, they are not subject to the restrictions on parameters and return values for server calls. User objects cannot, however, be objects constructed with the new operator
             */
            withUserObject(object: object): IRun;
        }
        ....
    }
}
```

この定義では、関数呼び出しであることはチェックできますが、存在しない関数を呼び出すコードや型がマッチしない引数を指定したコードも書けてしまいます。
`google.script.run` でサーバサイドの`myFunction`と言うサーバサイドの関数を呼び出す場合は `google.script.run.myFunction()` と書く必要があり、各開発者が自由に実装(定義)するサーバサイドのGASの関数を事前にd.tsに定義することはできないので仕方ないですね。

## サーバサイドのソースコード(*.ts)からクライアントサイドのd.tsを生成する

サーバサイドをTypeScriptで書いているのであれば、その型の情報からクライアントサイドの型定義(d.ts)を生成すればいいのでは？と思い [google-script-dts-generator](https://github.com/fossamagna/google-script-dts-generator) 生成ツールを作成しました。

サーバサイドのTypeScriptのソースコードからクライアントサイドのd.tsを生成するCLIツールです。

```sh
npm install google-script-dts-generator --save-dev
```

```sh
yarn add google-script-dts-generator -D
```

でインストールできます。

使い方は以下の通りで、型定義生成の元になるサーバサイドのソースコードのディレクトリと生成する型定義ファイルの出力先ディレクトリを指定して実行するだけです。

```sh
google-script-dts-generator -s <サーバサイドのソースコードのディレクトリのパス> -o <生成するd.tsを出力するディレクトリのパス>
```

例えば、サーバサイドのGASとして以下のようなコードの場合、

```typescript:server.ts
global.echo = (message: string) => {
  return message;
};
```

`google-script-dts-generator` を実行すると次のような型定義(google.script.d.ts)ファイルが生成されます。

```typescript:google.script.d.ts
declare namespace google {
    /**
     * Methods available to Google Apps Script
     */
    namespace script {
        interface IRun {
            echo(message: string): void;
            /**
             * Sets a callback function to run if the server-side function throws an exception. Without a failure handler, failures are logged to the JavaScript console. To override this, call withFailureHandler(null) or supply a failure handler that does nothing.
             * @param callback a client-side callback function to run if the server-side function throws an exception; the Error object is passed to the function as the first argument, and the user object (if any) is passed as a second argument
             */
            withFailureHandler(callback: (error: Error, object?: any)=>void): IRun;
            /**
             * Sets a callback function to run if the server-side function returns successfully.
             * @param callback a client-side callback function to run if the server-side function returns successfully; the server's return value is passed to the function as the first argument, and the user object (if any) is passed as a second argument
             */
            withSuccessHandler(callback: (value: any, object?: any)=>void): IRun;
            /**
             * Sets an object to pass as a second parameter to the success and failure handlers.
             * @param {Object} object an object to pass as a second parameter to the success and failure handlers; because user objects are not sent to the server, they are not subject to the restrictions on parameters and return values for server calls. User objects cannot, however, be objects constructed with the new operator
             */
            withUserObject(object: object): IRun;
        }
```

この生成された型定義を利用すれば以下のように、クライアントサイドの`google.script.run`で呼び出す関数の型もチェックできるようになります。

```typescript:index.ts(存在する関数で引数の型もマッチしている場合)
google.script.run
    .withSuccessHandler(v => {})
    .withFailureHandler(e => console.error(e))
    .echo("hello gas");
// OK (No Error)
```

```typescript:index.ts(引数の型違いの場合)
google.script.run
    .withSuccessHandler(v => {})
    .withFailureHandler(e => console.error(e))
    .echo(123);
// index.ts:4:11 - error TS2345: Argument of type '123' is not assignable to parameter of type 'string'.
//
// 4     .echo(123);;
//            ~~~
// Found 1 error.
```

```typescript:index.ts(存在しない関数の場合)
google.script.run
    .withSuccessHandler(v => {})
    .withFailureHandler(e => console.error(e))
    .echoo("hello gas");
// index.ts:4:6 - error TS2551: Property 'echoo' does not exist on type 'IRun'. Did you mean 'echo'?
//
// 4     .echoo("hello gas");;
//        ~~~~~
//
//  google.script.d.ts:7:13
//    7             echo(message: string): void;
//                  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//    'echo' is declared here.
//
// Found 1 error.
```

このサンプルコードの全ては [GitHubのリポジトリ](https://github.com/fossamagna/google-script-dts-generator/tree/master/example) で確認できます。

## まとめ

`google-script-dts-generator` を使うとクライアントサイドの型定義(google.script.d.ts)ファイルを生成できます。生成した型定義ファイルでクライアントサイドの関数呼び出し時にも型のチェックもできるようになります。
GAS+TypeScriptでWebアプリを作成されている方にはぜひ一度使ってみてもらいたいです。機能追加要望やフィードバックなどいただけるとありがたいなと思います。