serverless framework DE typescript for AWS Lambda開発

###既に作ったserverlessプロジェクトにTypescriptを入れたい

現在AWSでSlack Botを動かすために、開発環境を作れるserverless絶賛勉強中です。
プロジェクトを作る時にtypescript対応するのは見つかったのですが、
もう作っちゃったプロジェクトに追加する方法は見当たらなかったのでまとめます。

プロジェクト作成時の場合はcreateするときに`-t aws-nodejs-typescript`というテンプレートを指定すればいいらしい。次はそっちでやってみます。
参考：[ServerlessでTypeScriptの開発環境を作る](https://qiita.com/iewori/items/837dbe4edb77426a11d9)

#serverlessプラグインを追加

https://www.serverless.com/plugins/serverless-plugin-typescript/
serverless-plugin-typescriptという素敵なプラグインを見つけたので使います。

>**Features**
・Zero-config: Works out of the box without the need to install any other compiler or plugins
・Supports ES2015 syntax + features (export, import, async, await, Promise, ...)
・Supports sls package, sls deploy and sls deploy function
・Supports sls invoke local + --watch mode
・Integrates nicely with serverless-offline

ローカル環境構築に使うserverless-offlineとの連携に自信ありとのことで、良いですね。
実際この後serverless-offlineも導入しましたが大体問題なく変換されています。

##インストール

```bash
npm install -D serverless-plugin-typescript typescript
```
した後に、お約束の`serverless.yml`のpluginsに下記を追加

```yaml
plugins:
  - serverless-plugin-typescript
```

##tsconfig.json
[minimum example](https://github.com/prisma-labs/serverless-plugin-typescript/tree/master/example)を見る限り、デフォルトで良ければtsconfig.json作らなくても動くっぽい。

```json:tsconfig.json
{
  "compilerOptions": {
    "preserveConstEnums": true,
    "strictNullChecks": true,
    "sourceMap": true,
    "allowJs": true,
    "target": "es5",
    "outDir": ".build",
    "moduleResolution": "node",
    "lib": ["es2015"],
    "rootDir": "./"
  }
}
```
これがデフォルトで、`outDir`と`rootDir`以外は上書きしていいよ！とのことです。
私はTypescript、細かいとこまで気が付くデキるやつだなぁ…くらいのノリで使ってるので、今のところ特に何も変更してません。

**あとはsls deploy時に勝手にトランスパイルしてくれます。**
(ディレクトリ含むファイル分割しても勝手に上手いことしてくれるのかはこれから検証…)

##serverless-offlineと使う

時はserverless-plugin-typescriptが先に来るように`serverless.yml`に記述しないとダメらしいです。

```yaml:serverless.yml
  plugins:
    ...
    - serverless-plugin-typescript
    ...
    - serverless-offline
    ...
```
`serverless-dynamodb-local`を使う時もなんか順番があるみたいですが、私はまだ使ってないのでよくわかりません。[各自ご確認ください](https://www.serverless.com/plugins/serverless-plugin-typescript/)

##aws-sdkの型定義ファイルを使う
他で使われていたので[@types/aws-sdk](https://www.npmjs.com/package/@types/aws-sdk)というパッケージを見に行ったら、もう公式が提供してるから使わなくていいよと言われてしまったので公式を見に行きます。


https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/#Usage_with_TypeScript
まずは、というか実際`@types/node`をインストールするだけです。

```bash
npm install --save-dev @types/node
```

あとは.tsファイルからの読み込みを書き換えて完了！

```ts
import AWS from 'aws-sdk';
```

##なぜかSlack Botが動かなくなった…

動かなくなりました。公式の言う通りimportした後、こんな感じで`AWS`オブジェクトの中のLambdaメソッドを呼び出していたのですが、
Lambdaはundifinedを参照していますみたいなエラーが出て、なぜか`AWS`の中身がからっぽということに…どうして、さっきまでこれで動いていたのに。

```ts:handler.ts
import AWS from 'aws-sdk';
const lambda = new AWS.Lambda()
```

serverless-offlineを使うと？distフォルダにコンパイル後のjsファイルが生成されているので、そこを見てみるとこんなことになってました。
勝手に入ってきた`.default`君、何？

```js:handler.js
var aws_sdk_1 = require("aws-sdk");
var lambda = new aws_sdk_1.default.Lambda();
```
`.default`君がどこから何のために来たのか全然分からないんですが、とりあえずimport時に直接Lambdaメソッドを分割代入したら無事動いてくれました。

→12/02追記
あっ分かりました、分割代入ではなくimportするとdefault exportされた結果のdeaultというオブジェクトとしてimportされるんですね～…

```ts:handler.ts
import { Lambda } from 'aws-sdk';
const lambda = new Lambda()
```

##型定義を付けた値にserverless.ymlの環境変数を入れる時の注意
Lambda.invoke()に渡すパラメーターを設定する変数に型定義`AWS.Lambda.InvocationRequest`を付けた時のことです。


```ts
  const params: AWS.Lambda.InvocationRequest = {
    FunctionName: process.env.INVOKE_FUNCTION,
    InvocationType: "RequestResponse",
    Payload: JSON.stringify({ msg: "Hello" }),
  };
```

`process.env.INVOKE_FUNCTION`のとこで下記のような、エラーがコンソールに湧き出ました。

```bash
api-lambda.ts (13,17): Argument of type '{ FunctionName: string | undefined; InvocationType: string; Payload: string; }' is not assignable to parameter of type 'InvocationRequest'.
  Types of property 'FunctionName' are incompatible.
    Type 'string | undefined' is not assignable to type 'string'.
      Type 'undefined' is not assignable to type 'string'.
```

ちなみにinvokeする関数のFunctionNameは、`serverless.yml`で定義している「サービス名-ステージ名-functionsで定義した関数名」で生成されるので、`serverless.yml`でファイル内参照しながら書く方が楽なんですよね。こんな感じで。
`"${self:service}-${self:provider.stage}-<functions内で定義した関数名>"`

まぁそれはともかく、エラーは「string型で定義したところに'string | undefined'の値は入れられません」とか言ってます。
どうやら.yml内から持ってきた値はtypescriptの方で中身がある無しを判断できないので自動的に'string | undefined'型と判定されるようです。

しかしそんなときの為にTypescriptさんが解決策を用意してくれていました

```ts
  const params: AWS.Lambda.InvocationRequest = {
    FunctionName: process.env.INVOKE_FUNCTION!,
    ...
  };
```

これで解決です。`!`つけるだけ。
[Non-null assertion operator](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html#non-null-assertion-operator)
要するにこれは末尾に付けた変数がnullでもundefinedでもありませんよというのを明示的に指定できる演算子のようです。

###?を付ければOptional chaining
そういえば`?`をオブジェクトの末尾に付けると、途中でundifinedの中身を参照しようとしてもエラーを出さずにnullが帰ってくるというめちゃくちゃ良い感じの機能がありますね。
slackのレスポンスはオブジェクトがネストしてることも多いので、私が今のところそこまでよく分かってないtypescriptを入れたい理由の一つです。
https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#optional-chaining

以上です。