仕事でリリースオペレーションの一部にSlackとHubotによるChatOpsを利用していますが、Hubotの開発言語はCoffeeScriptですね。
Googleトレンドからしても分かるように開発は下火になっていると考えられ、若手にはかえって分かりづらいので、今後はTypeScriptを押していきたいです。

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 、 型定義利用のために @types/hubot をインストールします。
npm i -D typescript @types/hubot
package.json
  "devDependencies": {
+    "typescript": "^3.9.5",
+    "@types/hubot": "^3.3.0"
  },
ファイル変更を監視してTypeScript→JavaScript変換コマンドを追加
package.json
  "scripts": {
+    "watch": "./node_modules/typescript/bin/tsc -w"
  }
tsconfig.jsonにコンパイルの設定を追加。 typescripts/ に書いたコードは scripts/ に書き出されます。
※その他の設定はサクッと他所からコピペしたものなので、細かい設定内容はこれからチューニングしていきたいと思います。
{
  "compilerOptions": {
    "target": "ES2019",
    "module": "commonjs",
    "outDir": "./scripts/",
    "rootDir": "./typescripts/",
    "strict": true,
    "esModuleInterop": true
  },
  "exclude": [
    "node_modules"
    ]
}
著者のSlackのハンドルネームを kisshy を kissy とタイポされると自動的に修正するHubotスクリプトがあったので、試しにTypeScript化してみます。
import hubot from 'hubot'
module.exports = (robot: hubot.Robot): void => {
  robot.hear(/kissy/, (msg: hubot.Response) => {
    msg.send(':no_good: kissy :ok_woman: kisshy')
  })
}
↓これが、こんなふうに変換されました。
"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を愛用しているのですが、
こんな感じでメソッド補完候補を出してくれたり
 
メソッドの引数を予め表示してくれたり
 
変数のスペルミスや型の不一致がコンパイルエラーになります。
 
 
かなりBot開発が快適になる予感しかしません。
ただ、
- 
typescripts/*.tsファイルとscripts/*.jsファイルを一緒にコミットしないといけない
- 間違えて scripts/*.jsのほうを修正しちゃう
このように運用上ちょっとだけ注意が必要なので、お気をつけください。
最終的に scripts/ 以下を全て typescripts/ 以下に移してしまえば、もはや scripts/ はgitignoreしてしまって、デプロイ時にtscでコンパイルすればスッキリすると思うので、そういう状態を目指してじわじわ移行していきたいと思います。
TypeScriptにしてよかったこと
- 型宣言がコード補完に使えるので、Hubotライブラリで何ができるのか、以前よりわかりやすくなった。
- 
asyncawaitで非同期処理が簡潔に書けた(特にGitHubからのレスポンスチェック→コールバックの処理)
- 
export default class Hoge {...}とファイルに分けてimportするだけで処理分割が簡単に行うことができ、見通しの良いコードが書けた
※一部ES2020でも賄えそうな話もありますが、ご容赦頂ければ。
TypeScriptにして悪かったこと
型宣言が存在しないライブラリがHubotを拡張していると、存在しないプロパティ参照でコンパイルエラーになってしまう
 
今回書いた処理の中で、hubot-slackというライブラリを併用して robot.adapter.client.web.chat.postMessage(...) という記述でチャットに返信するという処理を記述する必要がありました。
しかし、hubot-slackには型宣言が存在しないので、上記の robot という変数の型を robot: hubot.Robot と宣言すると robot.adapter.client でコンパイルエラーとなってしまいます。
この場合は、やむなく型宣言を robot: any とするしかありませんでした。(コンストラクタ引数ではちゃんと型宣言しているので、違う型のオブジェクトがセットされることはありません)
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