TypeScript を使って Alexa Custom Skills を作ろう (3) 実装 - ハンドラ

# はじめに
この投稿からサンプルスキルを [(2)設計](https://qiita.com/daisukeArk/items/c22f7f4e4f231d05dd51) に合わせて TypeScript へ変換していきます。

# 変換元となるサンプルスキルについて
インターネットに公開されているカスタムスキルのソースは、便宜上 index.js にすべての処理が書かれているのを多く見かけます。
私が作成したサンプルスキルも同じように index.js にすべての処理を書きました。
これを [(2)設計](https://qiita.com/daisukeArk/items/c22f7f4e4f231d05dd51) に合わせて TypeScript へ変換していきます。

[変換元サンプルスキルソース(github)](https://github.com/daisukeArk/custom-skill-sample-to-convert/tree/dev/export-base)
※ちなみにこのソースは、スキルとしては微妙です。
「こんにちは」と言ったり、数字を言わせてただ「何番です。」と返すだけ、使っても得られるものは何もないでしょう。

# さあ、変換しよう
今回の投稿では、ハンドラを TypeScript へ変換していきます。
ハンドラに定義されている Intent（物事）や Utterance(発話)は、次回以降で変換を行っていきます。
※エラーが発生しない最低限のところまでは変換していきます。

## サンプルソースを準備
サンプルを任意のディレクトリにダウンロードもしくはクローンする。
ここでは、ホームディレクトリの custom-skill-sample-to-convert にクローンしたものとして進めます。

## カレントディレクトリの移動

```bash:移動
$ cd ~/custom-skill-sample-to-convert/skill/lambda/custom
```

## TypeScript 導入

```bash:TypeScriptインストール
$ npm install --save-dev typescript
```

```bash:リンターインストール
$ npm install --save-dev tslint
```

```bash:型定義ファイルの取得
$ npm install --save-dev @types/node @types/alexa-sdk
```

```bash:TypeScript初期設定
$ $(npm bin)/tsc --init
```

```bash:リンター初期設定
$ $(npm bin)/tslint --init
```

## tsconfig.jsonの修正

`tsc --init` でtsconfig.jsonがいくつかのオプションがコメントアウトされた状態で作成されます。
以下の設定を追記または修正をします。

```json:tsconfig.json（抜粋）
{
  "compilerOptions": {
    /* Basic Options */

    // 出力するECMAScriptのバージョン
    "target": "ES2015",
    // マップファイル
    "sourceMap": true,
    // コンパイル後、出力先
    "outDir": "./dist",

    /* Strict Type-Checking Options */
    "strict": true,
    // 暗黙のanyはエラー扱い
    "noImplicitAny": true,
    // use strict出力
    "alwaysStrict": true,

    /* Additional Checks */
    // ファンクションが戻り値を正しく返していなければエラー
    "noImplicitReturns": true,
    //　switchステートメントエラー
    "noFallthroughCasesInSwitch": true,

    /* Module Resolution Options */

    /* Source Map Options */

    /* Experimental Options */
  },
  "include": [
    "src/**/*"
  ],
  "exclude": [
    "node_modules",
    "**/*.spec.ts"
  ]
}
```

## tslint.jsonの修正

`tslint --init` でtslint.jsonが作成されます。

プリセットは`tslint:recommeded`を使用します。
設定はお好みで変えてください

```json:tslint.json
{
  "defaultSeverity": "error",
  "extends": [
    "tslint:recommended"
  ],
  "jsRules": {},
  "rules": {
    "object-literal-key-quotes": false,
    "object-literal-shorthand": false,
    "object-literal-sort-keys": false,
    "quotemark": [
      true,
      "single",
      "jsx-double"
    ],
    "space-before-function-paren": [
      true,
      {
        "anonymous": "always",
        "named": "never"
      }
    ],
    "trailing-comma": [
      true,
      {
        "singleline": "never",
        "multiline": "never"
      }
    ]
  },
  "rulesDirectory": []
}
```

## ディレクトリ構成を変更
ソースファイルとコンパイル後出力先を分けて管理するため、ディレクトリを作成します。
既存のソースファイル(index.js)をsrcディレクトリへ移動します。

```bash:ディレクトリ作成
$ mkdir -p ./src ./dist
```

## index.js をTypeScriptへ変換します。

```bash:index.jsをディレクトリ構成変更ついでに、TypeScriptファイルへ変更
$ git mv ./index.js ./src/index.ts
```

ファイル格納先を変更して、拡張子を変更しただけなので、このままトランスパイルしても暗黙的な"any"を許可していないためエラーとなってしまいます。

```bash:トランスパイル（エラー）
$ $(npm bin)/tsc
src/index.ts(4,28): error TS7006: Parameter 'event' implicitly has an 'any' type.
src/index.ts(4,35): error TS7006: Parameter 'context' implicitly has an 'any' type.
src/index.ts(44,10): error TS2339: Property 'handler' does not exist on type '{ 'LaunchRequest': () => void; 'AMAZON.HelpIntent': () => void; 'Unhandled': () => void; }'.
src/index.ts(45,10): error TS2339: Property 'emitWithState' does not exist on type '{ 'LaunchRequest': () => void; 'AMAZON.HelpIntent': () => void; 'Unhandled': () => void; }'.
src/index.ts(48,10): error TS2339: Property 'emit' does not exist on type '{ 'LaunchRequest': () => void; 'AMAZON.HelpIntent': () => void; 'Unhandled': () => void; }'.
src/index.ts(48,28): error TS2339: Property 't' does not exist on type '{ 'LaunchRequest': () => void; 'AMAZON.HelpIntent': () => void; 'Unhandled': () => void; }'.
src/index.ts(51,10): error TS2339: Property 'emit' does not exist on type '{ 'LaunchRequest': () => void; 'AMAZON.HelpIntent': () => void; 'Unhandled': () => void; }'.
src/index.ts(51,28): error TS2339: Property 't' does not exist on type '{ 'LaunchRequest': () => void; 'AMAZON.HelpIntent': () => void; 'Unhandled': () => void; }'.
src/index.ts(98,23): error TS7006: Parameter 'number' implicitly has an 'any' type.
src/index.ts(100,13): error TS7017: Element implicitly has an 'any' type because type '{ "1": string; "2": string; "3": string; }' has no index signature.
```

まずは、上から順番にエラーを解決していきましょう。

## use strict, require, exports.handler部分

```javascript:index.ts
'use strict';
var Alexa = require('alexa-sdk');

exports.handler = function(event, context, callback) {
  var alexa = Alexa.handler(event, context);
  alexa.appId = process.env.APP_ID || 'xxxx';
  alexa.resources = languageStrings;
  alexa.registerHandlers(newSessionHandler, startHandler, firstHandler);
  alexa.execute();
};
```

### use strict

~~'use strict';~~

`'use strict';`文は、tsconfig.jsonで指定した `alwaysStrict`オプションによってソースファイル毎に常に出力される為、削除します。

### require

alexa-sdkで定義されている全てのモジュールを変数にインポートします。

```typescript:index.ts（変更前）
var Alexa = require('alexa-sdk');
```

```typescript:index.ts（変更後）
import * as Alexa from 'alexa-sdk';
```

### exports.handler

```typescript:index.ts（変更前）
exports.handler = function(event, context) {
  var alexa = Alexa.handler(event, context);
  alexa.appId = process.env.APP_ID;
  alexa.resources = languageStrings;
  alexa.registerHandlers(newSessionHandler, startHandler, firstHandler);
  alexa.execute();
};
```

明示的に型を指定していきます。
修正部分は１行目だけですね。

```typescript:index.ts（変更後）
export const handler = (event: Alexa.RequestBody<any>, context: Alexa.Context, callback: (err: any, response: any) => void) => {
  const alexa = Alexa.handler(event, context);
  if (process.env.APP_ID) { alexa.appId = process.env.APP_ID; }
  alexa.resources = languageStrings;
  alexa.registerHandlers(newSessionHandler, startHandler, firstHandler);
  alexa.execute();
};
```

## 各ハンドラ部分

```typescript:index.ts（変更前）
const newSessionHandler = {
  'LaunchRequest': function() {
    this.handler.state = handlerStates.START_MODE;
    this.emitWithState('Start');
  },
  'AMAZON.HelpIntent': function() {
    this.emit(':ask', this.t('ASK_HELP_MESSAGE'));
  },
  'Unhandled': function() {
    this.emit(':ask', this.t('ASK_UNHANDLED_MESSAGE'));
  }
};
```

これも型を明示的に指定していきます。

```typescript:index.ts（変更後）
const newSessionHandler: Alexa.Handlers<any> = {
  'LaunchRequest': function (this: Alexa.Handler<any>) {
    this.handler.state = handlerStates.START_MODE;
    this.emitWithState('Start');
  },
  'AMAZON.HelpIntent': function (this: Alexa.Handler<any>) {
    this.emit(':ask', this.t('ASK_HELP_MESSAGE'));
  },
  'Unhandled': function (this: Alexa.Handler<any>) {
    this.emit(':ask', this.t('ASK_UNHANDLED_MESSAGE'));
  }
};
```

変更箇所は変数の宣言部とファンクションの引数部で型を明示的に指定している箇所です。
個人的にはちょっとこれがキモい。。。
ですが、thisがanyとなってしまう為、thisに対して型を明示的に指定しています。
他にやり方があればご教授ください！

他のハンドラは、長くなってしまう為変数宣言部のみ（先頭から２行）変更後を抜粋します。

```typescript
const startHandler: Alexa.Handlers<any> = Alexa.CreateStateHandler(handlerStates.START_MODE, {
  'Start': function (this: Alexa.Handler<any>) {

const firstHandler: Alexa.Handlers<any> = Alexa.CreateStateHandler(handlerStates.FIRST_MODE, {
  'FirstIntent': function (this: Alexa.Handler<any>) {
```

## あと、すこし

連想配列、ファンクションの引数も型を明示的に指定します。

```typescript:index.ts（変更前）
const newsContents = {
  "1": "1番です",
  "2": "2番です",
  "3": "3番です",
};

function getNewsAsync(number) {
  return new Promise(function(resolve, reject) {
    resolve(newsContents[number]);
  });
}
```

```typescript:index.ts（変更後）
const newsContents: {[key: string]: string} = {
  '1': '1番です',
  '2': '2番です',
  '3': '3番です',
};

function getNewsAsync(sayNumber: string) {
  return new Promise((resolve, reject) => {
    resolve(newsContents[sayNumber]);
  });
}
```

一通り、変換しました。
ここまででもう一度トランスパイルしてみましょう。

```bash:トランスパイル（成功！）
$ $(npm bin)/tsc
```

## ハンドラを index.ts から外出しする

さて、いよいよ本題のハンドラ部分を外部ファイル化していきます。
まずは、ディレクトリと空ファイルを作りましょう

```bash:移動
$ cd ~/custom-skill-sample-to-convert/skill/lambda/custom/src
```

```bash:handlersディレクトリ作成
$ mkdir -p handlers
```

```bash:enumsディレクトリ作成
$ mkdir -p enums
```

```bash:状態種別ファイル作成
$ touch ./enums/handler-state-types.ts
```

```bash:ハンドラファイル作成
$ touch ./handlers/new-session-handler.ts
$ touch ./handlers/start-handler.ts
$ touch ./handlers/first-handler.ts
```

次に、状態種別を外部ファイル化します。
index.ts から以下部分を切り取りしtsファイルへ移植します。

```typescript:index.ts
const handlerStates = {
  NONE: '',
  START_MODE: '_START_MODE',
  FIRST_MODE: '_FIRST_MODE'
};
```

```typescript:enums/handler-state-types.ts
export enum HandlerStateTypes {
  NONE = '',
  START_MODE = '_START_MODE',
  FIRST_MODE = '_FIRST_MODE'
};
```

ハンドラー部分も同じように移植していきます。
切り取りするコードは長くなるので、割愛します。

```typescript:handlers/new-session-handler.ts
import * as Alexa from 'alexa-sdk';
import { HandlerStateTypes } from '../enums/handler-state-types';

export const handler: Alexa.Handlers<any> = {
  // ~~~省略~~~
};
```

```typescript:handlers/start-handler.ts
import * as Alexa from 'alexa-sdk';
import { HandlerStateTypes } from '../enums/handler-state-types';

export const handler: Alexa.Handlers<any> = Alexa.CreateStateHandler(HandlerStateTypes.START_MODE, {
  // ~~~省略~~~
});
```

```typescript:handlers/first-handler.ts
import * as Alexa from 'alexa-sdk';
import { HandlerStateTypes } from '../enums/handler-state-types';

export const handler: Alexa.Handlers<any> = Alexa.CreateStateHandler(HandlerStateTypes.FIRST_MODE, {
  // ~~~省略~~~
});

const newsContents: {[key: string]: string} = {
  "1": "1番です",
  "2": "2番です",
  "3": "3番です",
};

function getNewsAsync(sayNumber: string) {
  return new Promise((resolve, reject) => {
    resolve(newsContents[sayNumber]);
  });
}
```

追記した部分は、以下の3つです。

- 1〜2行目のimport文(alexa-sdkと外部ファイル化した状態種別のインポート)
- 4行目の宣言部（外部モジュールとしてエクスポート）
- 状態種別の変数名の変更（`handlerStates`から`HandlerStateTypes`へ変更）

最後に外部ファイル化したハンドラをindex.tsでインポートします（２〜４行目まで）。

```typescript:index.ts
import * as Alexa from 'alexa-sdk';
import { handler as firstHandler } from './handlers/first-handler';
import { handler as newSessionHandler } from './handlers/new-session-handler';
import { handler as startHandler } from './handlers/start-handler';

export const handler = (event: Alexa.RequestBody<any>, context: Alexa.Context, callback: (err: any, response: any) => void) => {
  const alexa = Alexa.handler(event, context, callback);
  if (process.env.APP_ID) { alexa.appId = process.env.APP_ID; }
  alexa.resources = languageStrings;
  alexa.registerHandlers(newSessionHandler, startHandler, firstHandler);
  alexa.execute();
};

const languageStrings = {
  // ~~~省略~~~
};
```

最後にトランスパイルしましょう！

```bash:トランスパイル（成功！）
$ $(npm bin)/tsc
```

# まとめ
この投稿では、index.ts からハンドラ部分を切り出し、外部モジュール化を行いました。
ハンドラ部分を外部モジュール化することによって、index.tsがだいぶすっきりしましたね。
複数ファイルに分けることで、全体の見通しがよくなったと思います。

今回の投稿のソースは以下にあります。
[TypeScript変換](https://github.com/daisukeArk/custom-skill-sample-to-convert/tree/dev/convert-index)
[ハンドラ外部モジュール化](https://github.com/daisukeArk/custom-skill-sample-to-convert/tree/dev/export-handlers)

次回は、[インテント処理](https://qiita.com/daisukeArk/items/53a45115d1530cb8782f)です！