« Docker for Windowsでコンテナイメージの置き場所を変える | トップページ | それさえもおそらくは平穏なWindows 10 Mobile »

2018年2月12日 (月)

AlexaのカスタムスキルをTypeScriptで書く

やってみたことの覚え書き。 AWS Lambda上で動かすAlexaのカスタムスキルをTypeScriptで書くにはどうすればよいか。

例として、「Alexaスキル開発トレーニングシリーズ 第1回 初めてのスキル開発」の「宇宙豆知識」をTypeScriptで書いてみます。

npmパッケージを追加する

javascriptでAWS Lambda上で動かすスキルを書く場合、 以下のnpmパッケージをインストールしていることでしょう。

  • aws-sdk
  • alexa-sdk

これに加えて、以下のTypeScript用の型情報パッケージをインストールします。

  • @types/node
  • @types/aws-lambda
  • @types/alexa-sdk

"@types/node"はNode.jsの機能を使う場合に必要になります。 例えば、スキル実装の定石として、以下のようなコードがあります。 アプリケーションIDを環境変数"APP_ID"から読み込むものです。

    const alexa = Alexa.handler(event, context);
    alexa.appId = process.env.APP_ID;

このprocessはNode.jsのオブジェクトですので、 「process.env」をTypeScript上で型解決するには"@types/node"が必要になります。

"@types/aws-lambda"はLambdaの機能を利用する場合に必要となります。 しかし、単純なスキルの実装では、 Lambdaの機能を明に利用することはほとんどないでしょうから、 不要かもしれません。

型を利用してプログラムする

型情報を利用して、 「Alexaスキル開発トレーニングシリーズ 第1回 初めてのスキル開発」の「宇宙豆知識」のindex.jsをTypeScriptで書くと、 以下のようになるんじゃないかな。

import * as Alexa from 'alexa-sdk';

//=========================================================================================================================================
//TODO: このコメント行より下の項目に注目してください。
//=========================================================================================================================================

//Replace with your app ID (OPTIONAL).  You can find this value at the top of your skill's page on http://developer.amazon.com.  
//Make sure to enclose your value in quotes, like this: var APP_ID = "amzn1.ask.skill.bb4045e6-b3e8-4133-b650-72923c5980f1";
const APP_ID = undefined;

const SKILL_NAME = "豆知識";
const GET_FACT_MESSAGE = "知ってましたか?";
const HELP_MESSAGE = "豆知識を聞きたい時は「豆知識」と、終わりたい時は「おしまい」と言ってください。どうしますか?";
const HELP_REPROMPT = "どうしますか?";
const STOP_MESSAGE = "さようなら";

//=========================================================================================================================================
//「TODO: ここから下のデータを自分用にカスタマイズしてください。」
//=========================================================================================================================================
const data = [
    "水星の一年はたった88日です。",
    "金星は水星と比べて太陽より遠くにありますが、気温は水星よりも高いです。",
    "金星は反時計回りに自転しています。過去に起こった隕石の衝突が原因と言われています。",
    "火星上から見ると、太陽の大きさは地球から見た場合の約半分に見えます。",
    "木星の<sub alias='いちにち'>1日</sub>は全惑星の中で一番短いです。",
    "天の川銀河は約50億年後にアンドロメダ星雲と衝突します。",
    "太陽の質量は全太陽系の質量の99.86%を占めます。",
    "太陽はほぼ完璧な円形です。",
    "皆既日食は一年から二年に一度しか発生しない珍しい出来事です。",
    "土星は自身が太陽から受けるエネルギーの2.5倍のエネルギーを宇宙に放出しています。",
    "太陽の内部温度は摂氏1500万度にも達します。",
    "月は毎年3.8cm地球から離れていっています。"
];

//=========================================================================================================================================
//この行から下のコードに変更を加えると、スキルが動作しなくなるかもしれません。わかる人のみ変更を加えてください。  
//=========================================================================================================================================
export const handler = function<T extends Alexa.Request>(event: Alexa.RequestBody<T>, context: Alexa.Context, callback?: (err: any, response: any) =< void) {
    const alexa = Alexa.handler(event, context);
    alexa.appId = APP_ID;
    alexa.registerHandlers(handlers);
    alexa.execute();
};

const handlers: Alexa.Handlers<Alexa.Request> = {
    'LaunchRequest': function (this: Alexa.Handler<Alexa.LaunchRequest>): void {
        this.emit('GetNewFactIntent');
    },
    'GetNewFactIntent': function (this: Alexa.Handler<Alexa.IntentRequest>): void {
        const factArr = data;
        const factIndex = Math.floor(Math.random() * factArr.length);
        const randomFact = factArr[factIndex];
        const speechOutput = GET_FACT_MESSAGE + randomFact;
        this.emit(':tellWithCard', speechOutput, SKILL_NAME, randomFact)
    },
    'AMAZON.HelpIntent': function (this: Alexa.Handler<Alexa.IntentRequest>): void {
        const speechOutput = HELP_MESSAGE;
        const reprompt = HELP_REPROMPT;
        this.emit(':ask', speechOutput, reprompt);
    },
    'AMAZON.CancelIntent': function (this: Alexa.Handler<Alexa.IntentRequest>): void {
        this.emit(':tell', STOP_MESSAGE);
    },
    'AMAZON.StopIntent': function (this: Alexa.Handler<Alexa.IntentRequest>): void {
        this.emit(':tell', STOP_MESSAGE);
    }
};

ポイントとしては、以下の通り。

  • varをconstに変更しているけど、これはTypeScriptに限らず、今となってはほとんどのvarはconst/letにした方がよい。
  • javascriptの"require('alexa-sdk')"はTypeScriptのimport文に変更する。
  • 唯一エクスポートする"handler"に"export"指定を追加。
  • 登録するハンドラを正しく型付ける。これにより、ハンドラ内の実装で型による検証が効くようになる。

で、TypeScriptで型付けを行った効果として、 潜在していたバグを見つけました。 上のソースで以下のように書いている部分、

    alexa.appId = APP_ID;

Alexaスキル開発トレーニングシリーズ 第1回 初めてのスキル開発」のオリジナルのサンプルでは、

    alexa.APP_ID = APP_ID;

となっています。 "alexa.APP_ID"は"alexa.appId"の間違いでしょう。 javascriptだとこの手の間違いは文法上間違いではないので、なかなか顕在化しませんが、 TypeScriptだと翻訳段階(チェック機能のあるエディタだとコーディング段階)であっさりと検出できます。

なので、TypeScriptの方が開発効率も安全性も向上すると思うんだけどなあ。

おまけ: インテントオブジェクトへのアクセス

多くの場合、 インテントのハンドラでは、インテントの内容を見なければなりません。 例えば、スロットの値を取得したりします。

    'MyIntent': function () {
        let value = this.event.request.intent.slots.MySlot.value; // スロットMySlotを参照
        ...

これを単純にTypeScriptで型付けすると、エラーになります。

    'MyIntent': function (this: Alexa.Handler<Alexa.IntentRequest>): void {
        let value = this.event.request.intent.slots.MySlot.value; // スロットMySlotを参照
        ...
[ts] オブジェクトは 'undefined' である可能性があります。

要するに、 "this.event.request"はAlexa.IntentRequestインターフェイス型なのですが、 その"intent"プロパティの定義は「Alexa.Intentインターフェイス型、またはundefined」となっており、 undefinedの場合"intent.slots"がエラーになる、と言っています。

この場合、このハンドラが呼び出されている時点でintentはundefinedでないだろうから、 以下のように書けばよいでしょう。 (intentが万一undefinedならば実行エラーとなる)

    'MyIntent': function (this: Alexa.Handler<Alexa.IntentRequest>): void {
        const intent = this.event.request.intent as Alexa.Intent;
        let value = intent.slots.MySlot.value; // スロットMySlotを参照
        ...

« Docker for Windowsでコンテナイメージの置き場所を変える | トップページ | それさえもおそらくは平穏なWindows 10 Mobile »

覚え書き」カテゴリの記事

作業記録」カテゴリの記事

コメント

コメントを書く

(ウェブ上には掲載しません)

トラックバック


この記事へのトラックバック一覧です: AlexaのカスタムスキルをTypeScriptで書く:

« Docker for Windowsでコンテナイメージの置き場所を変える | トップページ | それさえもおそらくは平穏なWindows 10 Mobile »