HubotをTypeScriptで書く

仕事でリリースオペレーションの一部にSlackと[Hubot](https://hubot.github.com/)によるChatOpsを利用していますが、Hubotの開発言語はCoffeeScriptですね。

Googleトレンドからしても分かるように開発は下火になっていると考えられ、若手にはかえって分かりづらいので、今後はTypeScriptを押していきたいです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128503/48697789-cf44-581b-87eb-791fdfc9eb24.png)
Googleトレンド(2020/08/18)
https://trends.google.co.jp/trends/explore?date=today%205-y&q=%2Fm%2F0hjc5m0,%2Fm%2F0n50hxv


最近リリースオペレーション改善のために「Slackに反応してGitHub APIをほげほげしてSlackに返事する」という機能を作りました。

その際、軽く開発規模を見積もったところ百数十行程度必要そうで、「この規模の処理を今更CoffeeScriptで開発したくないなぁ」という気持ちが強くなってTypeScriptで書くことを検討してみました。

# HubotをTypeScriptで書けるようにする

- CoffeeScriptってJavaScriptも読めるので、最終的にJavaScriptファイルが `scripts/` に置いてあればいいんじゃないか。
- じゃあ、 `*.ts` ファイルをコンパイルして `scripts/` ファイルにして一緒にコミットすればいいんじゃないか。

ということでやってみたら意外とサクッとできました。

## やってみた

まずは、コンパイル用に [`typescript`](https://www.npmjs.com/package/typescript) 、 型定義利用のために [`@types/hubot`](https://www.npmjs.com/package/@types/hubot) をインストールします。


```sh
npm i -D typescript @types/hubot
```

package.json

```diff
  "devDependencies": {
+    "typescript": "^3.9.5",
+    "@types/hubot": "^3.3.0"
  },
```

ファイル変更を監視してTypeScript→JavaScript変換コマンドを追加

package.json

```diff
  "scripts": {
+    "watch": "./node_modules/typescript/bin/tsc -w"
  }
```

tsconfig.jsonにコンパイルの設定を追加。 `typescripts/` に書いたコードは `scripts/` に書き出されます。

※その他の設定はサクッと他所からコピペしたものなので、細かい設定内容はこれからチューニングしていきたいと思います。

```json
{
  "compilerOptions": {
    "target": "ES2019",
    "module": "commonjs",
    "outDir": "./scripts/",
    "rootDir": "./typescripts/",
    "strict": true,
    "esModuleInterop": true

  },
  "exclude": [
    "node_modules"
    ]
}
```

著者のSlackのハンドルネームを `kisshy` を `kissy` とタイポされると自動的に修正するHubotスクリプトがあったので、試しにTypeScript化してみます。


```typescript:typescripts/correct-kisshy.ts
import hubot from 'hubot'

module.exports = (robot: hubot.Robot): void => {
  robot.hear(/kissy/, (msg: hubot.Response) => {
    msg.send(':no_good: kissy :ok_woman: kisshy')
  })
}
```

↓これが、こんなふうに変換されました。


```javascript:scripts/correct-kisshy.js
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
module.exports = (robot) => {
    robot.hear(/kissy/, (msg) => {
        msg.send(':no_good: kissy :ok_woman: kisshy');
    });
};
```

### 余談

ちなみに、このスクリプトを運用してもタイポは一向に減らず、むしろBotを発動させて遊ぶためだけにわざと間違える人が後を絶たなかったので、Slackのハンドルネームの方を `kisshy` → `kissy` に改名しました…😇

なので、現在ではこのようなBotは存在しません。

## coc.vimによる補完例

著者はエディタにNeoVimを利用しており、LanguageServerを利用したコード補完には[coc.vim](https://github.com/neoclide/coc.nvim)を愛用しているのですが、

こんな感じでメソッド補完候補を出してくれたり

<img src="https://cdn-ak.f.st-hatena.com/images/fotolife/R/Rakuma/20200629/20200629141556.png">

メソッドの引数を予め表示してくれたり

<img src="https://cdn-ak.f.st-hatena.com/images/fotolife/R/Rakuma/20200629/20200629141516.png">

変数のスペルミスや型の不一致がコンパイルエラーになります。

<img src="https://cdn-ak.f.st-hatena.com/images/fotolife/R/Rakuma/20200629/20200629141729.png">
<img src="https://cdn-ak.f.st-hatena.com/images/fotolife/R/Rakuma/20200629/20200629141700.png">


かなりBot開発が快適になる予感しかしません。

ただ、

- `typescripts/*.ts` ファイルと `scripts/*.js` ファイルを一緒にコミットしないといけない
- 間違えて `scripts/*.js` のほうを修正しちゃう

このように運用上ちょっとだけ注意が必要なので、お気をつけください。

最終的に `scripts/` 以下を全て `typescripts/` 以下に移してしまえば、もはや `scripts/` はgitignoreしてしまって、デプロイ時にtscでコンパイルすればスッキリすると思うので、そういう状態を目指してじわじわ移行していきたいと思います。

## TypeScriptにしてよかったこと

- 型宣言がコード補完に使えるので、Hubotライブラリで何ができるのか、以前よりわかりやすくなった。
- `async` `await` で非同期処理が簡潔に書けた（特にGitHubからのレスポンスチェック→コールバックの処理）
- `export default class Hoge {...}` とファイルに分けて `import` するだけで処理分割が簡単に行うことができ、見通しの良いコードが書けた

※一部ES2020でも賄えそうな話もありますが、ご容赦頂ければ。

## TypeScriptにして悪かったこと

### 型宣言が存在しないライブラリがHubotを拡張していると、存在しないプロパティ参照でコンパイルエラーになってしまう

<img src="https://cdn-ak.f.st-hatena.com/images/fotolife/R/Rakuma/20200629/20200629143545.png">

今回書いた処理の中で、[hubot-slack](https://github.com/slackapi/hubot-slack)というライブラリを併用して `robot.adapter.client.web.chat.postMessage(...)` という記述でチャットに返信するという処理を記述する必要がありました。

しかし、hubot-slackには型宣言が存在しないので、上記の `robot` という変数の型を `robot: hubot.Robot` と宣言すると `robot.adapter.client` でコンパイルエラーとなってしまいます。

この場合は、やむなく型宣言を `robot: any` とするしかありませんでした。（コンストラクタ引数ではちゃんと型宣言しているので、違う型のオブジェクトがセットされることはありません）

```typescript
export default class Message {
  robot: hubot.Robot //any // 本来はhubot.Robot型を宣言しておくが、hubot-slackにtypesが無いので、robot.adapter.clientがコンパイルエラーになるのを防ぐためにany
  roomToRespond: string

  constructor({ robot, roomToRespond }: { robot: hubot.Robot, roomToRespond: string }) {
    this.robot = robot
  }
```

---

今回はちょっとした小ネタでしたが、もし自分の組織でも使えそうであればぜひシェアして使ってみてください。

# 余談

HubotまわりはTypeScript時代へのメンテがちょっと置いていかれている感がありますね（特にhubot-slack）。
どこかの機会でBoltへの移行を検討してみてもいいかなと感じ、Boltもちょくちょくいじってみてます。
https://slack.dev/bolt-js/ja-jp/tutorial/hubot-migration