HubotをTypeScriptで書く

f:id:Rakuma:20200629153618p:plain

こんにちは、ラクマの岸です。

最近はマネージャ業の比率が多く、エンジニアリングの比率も下がってしまっているのでたまにこうしてひっそりと開発を楽しんでいます。

さて、ラクマではリリースオペレーションの一部にSlackとHubotによるChatOpsを利用していますが、Hubotの開発言語は「CoffeeScript」ですね。

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

最近リリースオペレーション改善のために「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のハンドルネームを kisshykissy とタイポされると自動的に修正するHubotスクリプトがあったので、試しに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')
  })
}

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

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のハンドルネームの方を kisshykissy に改名しました…😇

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

coc.vimによる補完例

著者はエディタにNeoVimを利用しており、LanguageServerを利用したコード補完にはcoc.vimを愛用しているのですが、

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

f:id:Rakuma:20200629141556p:plain

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

f:id:Rakuma:20200629141516p:plain

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

f:id:Rakuma:20200629141729p:plain f:id:Rakuma:20200629141700p:plain

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

ただ、

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

このように運用上ちょっとだけ注意が必要なので、お気をつけください。そしていい方法があればぜひ教えて下さい。

TypeScriptにしてよかったこと

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

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

TypeScriptにして悪かったこと

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

f:id:Rakuma:20200629143545p:plain

今回書いた処理の中で、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への移行を検討してみてもいいかなと感じました。

slack.dev

CM枠

余談ですが、TypeScriptを使ってフロントエンドの技術を刷新していく機運も徐々に高まっており、そんな気持ちに共感して頂ける仲間も募集しています。ご興味があればぜひ!カジュアル面談しましょう!

www.wantedly.com