LoginSignup
6
1

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-01-16

はじめに

この投稿からサンプルスキルを (2)設計 に合わせて TypeScript へ変換していきます。

変換元となるサンプルスキルについて

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

変換元サンプルスキルソース(github)
※ちなみにこのソースは、スキルとしては微妙です。
「こんにちは」と言ったり、数字を言わせてただ「何番です。」と返すだけ、使っても得られるものは何もないでしょう。

さあ、変換しよう

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

サンプルソースを準備

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

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

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

TypeScript 導入

TypeScriptインストール
$ npm install --save-dev typescript
リンターインストール
$ npm install --save-dev tslint
型定義ファイルの取得
$ npm install --save-dev @types/node @types/alexa-sdk
TypeScript初期設定
$ $(npm bin)/tsc --init
リンター初期設定
$ $(npm bin)/tslint --init

tsconfig.jsonの修正

tsc --init でtsconfig.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を使用します。
設定はお好みで変えてください

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ディレクトリへ移動します。

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

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

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

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

トランスパイル(エラー)
$ $(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部分

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で定義されている全てのモジュールを変数にインポートします。

index.ts(変更前)
var Alexa = require('alexa-sdk');
index.ts(変更後)
import * as Alexa from 'alexa-sdk';

exports.handler

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();
};

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

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();
};

各ハンドラ部分

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'));
  }
};

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

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に対して型を明示的に指定しています。
他にやり方があればご教授ください!

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

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>) {

あと、すこし

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

index.ts(変更前)
const newsContents = {
  "1": "1番です",
  "2": "2番です",
  "3": "3番です",
};

function getNewsAsync(number) {
  return new Promise(function(resolve, reject) {
    resolve(newsContents[number]);
  });
}
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]);
  });
}

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

トランスパイル(成功!)
$ $(npm bin)/tsc

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

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

移動
$ cd ~/custom-skill-sample-to-convert/skill/lambda/custom/src
handlersディレクトリ作成
$ mkdir -p handlers
enumsディレクトリ作成
$ mkdir -p enums
状態種別ファイル作成
$ touch ./enums/handler-state-types.ts
ハンドラファイル作成
$ touch ./handlers/new-session-handler.ts
$ touch ./handlers/start-handler.ts
$ touch ./handlers/first-handler.ts

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

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

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

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

export const handler: Alexa.Handlers<any> = {
  // ~~~省略~~~
};
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, {
  // ~~~省略~~~
});
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でインポートします(2〜4行目まで)。

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 = {
  // ~~~省略~~~
};

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

トランスパイル(成功!)
$ $(npm bin)/tsc

まとめ

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

今回の投稿のソースは以下にあります。
TypeScript変換
ハンドラ外部モジュール化

次回は、インテント処理です!

6
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
1