Boltを使ったSlack BotをTypescriptで開発する方法

# はじめに
今までslack用のbotを開発するのにhubotやerrbotを利用してきたのですが、昨年slackがリリースしたBoltフレームワークのチュートリアルをいくつかやってみて簡単にbot実装ができそうだなと思ったので、それをTypeScriptで実装する場合の方法を紹介します。

# Bolt開発時に参考にしたサイト
Boltを使ったslack botアプリの作成方法や実装サンプルなどは、下記を参考にさせて頂きました。

- [Bolt入門ガイド](https://slack.dev/bolt/ja-jp/tutorial/getting-started)
- [Bolt フレームワークを使って Slack Bot を作ろう](https://api.slack.com/lang/ja-jp/hello-world-bolt)

# 開発環境
主に下記の開発環境で実装及び動作確認をしました。

- VSCode
- node.js
- [Glitch](https://glitch.com/)

Glitchは、ウェブ上で動くIDEを持っているのですが、ローカルでVSCodeを使うことでコード補完やESLintなども使えるようになるのでローカル環境でVSCodeを利用して実装します。

# プロジェクトの作成
[Bolt入門ガイド](https://slack.dev/bolt/ja-jp/tutorial/getting-started)に記載のある通り、下記を実行しプロジェクトを作成します。

```shell-session
$ mkdir first-bolt-app
$ cd first-bolt-app
$ npm init -y
$ npm install @slack/bolt
```

typescript実装のために必要なパッケージをインストールします。

```shell-session
$ npm install -D typescript @types/node
```

その他ローカルで開発する上で利用するパッケージをインストールします。

```shell-session
$ npm install dotenv # .envファイルを読み込ませる為に追加
$ npm install -D eslint eslint-config-prettier eslint-plugin-prettier prettier @types/eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser
```

# 各種設定ファイルの作成
slackアプリのbot tokenとsigning secretを記載した`.env`ファイルを作成します。

```text:.env
SLACK_SIGNING_SECRET=<your-signing-secret>
SLACK_BOT_TOKEN=xoxb-<your-bot-token>
```

Typescriptプロジェクトをコンパイルするのに必要なコンパイラのオプションを指定する`tsconfig.json`ファイルを作成します。

```json:tsconfig.json
{
    "compilerOptions": {
        "module": "commonjs",
        "esModuleInterop": true,
        "allowSyntheticDefaultImports": true,
        "target": "es6",
        "noImplicitAny": true,
        "moduleResolution": "node",
        "sourceMap": true,
        "outDir": "dist",
        "baseUrl": ".",
        "paths": {
            "*": [
                "node_modules/*",
                "src/types/*"
            ]
        }
    },
    "include": [
        "src/**/*"
    ]
}
```

ESLintの設定ファイル`.eslintrc`と`.eslintignore`を作成します。

```text:.eslintrc
{
  "parser": "@typescript-eslint/parser",
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/eslint-recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:prettier/recommended",
    "prettier/@typescript-eslint"
  ],
  "parserOptions": {
    "ecmaVersion": 2018,
    "sourceType": "module"
  },
  "rules": {
    "semi": [
      "error",
      "always"
    ],
    "quotes": [
      "error",
      "double"
    ],
    "@typescript-eslint/explicit-function-return-type": "off",
    "@typescript-eslint/no-explicit-any": 1,
    "@typescript-eslint/no-inferrable-types": [
      "warn",
      {
        "ignoreParameters": true
      }
    ],
    "@typescript-eslint/no-unused-vars": "warn"
  }
}
```

```text:.eslintignore
# /node_modules/* in the project root is ignored by default
# build artefacts
dist/*
coverage/*
# data definition files
**/*.d.ts
# 3rd party libs
/src/public/
# custom definition files
/src/types/
```

tscコンパイルやbot実行の為に、`package.json`を下記の通りに変更します。

```json:package.json
{
〜省略〜
  "scripts": {
    "start": "npm run build && npm run serve",
    "build": "npx tsc",
    "serve": "node --require dotenv/config dist/app.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
〜省略〜
}
```

# コード作成と実行
ソースコードは`src/`以下に作成します。
下記は[Bolt入門ガイド](https://slack.dev/bolt/ja-jp/tutorial/getting-started)の「メッセージのリスニングと応答」にあるコードをTypescriptコードにしたものです。
ファイル拡張子が`.js`から`.ts`に変わったのとrequireをimport文に変えただけですね(笑)
もう少し複雑なサンプルコードを持ってくるとTypescriptの型エラーが発生し、適宜修正が必要になってきます。

```typescript:src/app.ts
import { App } from "@slack/bolt";

const app = new App({
  token: process.env.SLACK_BOT_TOKEN,
  signingSecret: process.env.SLACK_SIGNING_SECRET
});

// Listens to incoming messages that contain "hello"
app.message("hello", ({ message, say }) => {
  // say() sends a message to the channel where the event was triggered
  say(`Hey there <@${message.user}>!`);
});

(async () => {
  // Start your app
  await app.start(process.env.PORT || 3000);

  console.log("⚡️ Bolt app is running!");
})();
```

Typescriptのコードをコンパイルします。

```shell-session
$ npm run build

> first-bolt-app@1.0.0 build /Users/xxxx/first-bolt-app
> npx tsc
```

Javascriptに変換された`dist/app.js`が作成されます。

```javascript:dist/app.js
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
const bolt_1 = require("@slack/bolt");
const app = new bolt_1.App({
    token: process.env.SLACK_BOT_TOKEN,
    signingSecret: process.env.SLACK_SIGNING_SECRET
});
// Listens to incoming messages that contain "hello"
app.message("hello", ({ message, say }) => {
    // say() sends a message to the channel where the event was triggered
    say(`Hey there <@${message.user}>!`);
});
(() => __awaiter(void 0, void 0, void 0, function* () {
    // Start your app
    yield app.start(process.env.PORT || 3000);
    console.log("⚡️ Bolt app is running!");
}))();
//# sourceMappingURL=app.js.map
```

botを実行します。`⚡️ Bolt app is running!`が出力されれば成功です！

```shell-session
$ npm run serve

> first-bolt-app@1.0.0 serve /Users/xxxx/first-bolt-app
> node --require dotenv/config dist/app.js

⚡️ Bolt app is running!
```

コンパイルと実行を同時に行う場合は、`npm run start`を実行します。

```shell-session
$ npm run start

> first-bolt-app@1.0.0 start /Users/xxxx/first-bolt-app
> npm run build && npm run serve

> first-bolt-app@1.0.0 build /Users/xxxx/first-bolt-app
> npx tsc

> first-bolt-app@1.0.0 serve /Users/xxxx/first-bolt-app
> node --require dotenv/config dist/app.js

⚡️ Bolt app is running!
```

ここまで動作確認ができたらGlitchに作成したファイルアップすることでローカルマシンまでのトンネルするアプリの公開URLを用意できないと確認ができないslackからのevent受信などの確認もできるようになります。

```shell-session:アップするファイル一覧
first-bolt-app/
├── .env
├── package.json
├── src
│   └── app.ts
└── tsconfig.json
```

Glitch上でbotが起動するときは、`npm run start`が実行されるのでコンパイル済みの`dist/app.js`はアップしなくても動作します。

# おまけ
[Bolt フレームワークを使って Slack Bot を作ろう](https://api.slack.com/lang/ja-jp/hello-world-bolt)の「Learn More」の「もうちょっと複雑な Glitch サンプル」をTypescript実装したソースコードは下記においてますので参考になれば、幸いです。

- ソースコード: https://glitch.com/edit/#!/mixed-city
- Remixリンク: https://glitch.com/edit/#!/remix/mixed-city