はじめに
この投稿からサンプルスキルを (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 導入
$ npm install --save-dev typescript
$ npm install --save-dev tslint
$ npm install --save-dev @types/node @types/alexa-sdk
$ $(npm bin)/tsc --init
$ $(npm bin)/tslint --init
tsconfig.jsonの修正
tsc --init
で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
を使用します。
設定はお好みで変えてください
{
"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へ変換します。
$ 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部分
'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で定義されている全てのモジュールを変数にインポートします。
var Alexa = require('alexa-sdk');
import * as Alexa from 'alexa-sdk';
exports.handler
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行目だけですね。
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();
};
各ハンドラ部分
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'));
}
};
これも型を明示的に指定していきます。
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>) {
あと、すこし
連想配列、ファンクションの引数も型を明示的に指定します。
const newsContents = {
"1": "1番です",
"2": "2番です",
"3": "3番です",
};
function getNewsAsync(number) {
return new Promise(function(resolve, reject) {
resolve(newsContents[number]);
});
}
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
$ mkdir -p handlers
$ 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ファイルへ移植します。
const handlerStates = {
NONE: '',
START_MODE: '_START_MODE',
FIRST_MODE: '_FIRST_MODE'
};
export enum HandlerStateTypes {
NONE = '',
START_MODE = '_START_MODE',
FIRST_MODE = '_FIRST_MODE'
};
ハンドラー部分も同じように移植していきます。
切り取りするコードは長くなるので、割愛します。
import * as Alexa from 'alexa-sdk';
import { HandlerStateTypes } from '../enums/handler-state-types';
export const handler: Alexa.Handlers<any> = {
// ~~~省略~~~
};
import * as Alexa from 'alexa-sdk';
import { HandlerStateTypes } from '../enums/handler-state-types';
export const handler: Alexa.Handlers<any> = Alexa.CreateStateHandler(HandlerStateTypes.START_MODE, {
// ~~~省略~~~
});
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行目まで)。
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変換
ハンドラ外部モジュール化
次回は、インテント処理です!