How to Use TypeScript for AWS Lambda in 3 Steps
Tags: aws, typescript, javascript, tutorial
Published: 2020-06-10 05:19:34

# Situation

When we are using NodeJS as the runtime for AWS Lambdas, JavaScript is the default language. However, due to the lack of typing checking in JavaScript, from time to time, buggy code is deployed to Lambda inadvertently. Such as a small typo like this:

```javascript
exports.lambdaHandler = async (event, context) => {
  const queries = event.queytStringParameters;
  // ...
}
```

We mean to get queryStringParameters, but the queries ends up undefined as a result of carelessness.

# Goal
We want to utilize TypeScript to write lambda handlers. With TypeScript, we will have the following benefits:

- Vanilla code completion hints while programming
- Compilation time error checks to avoid redundant deployments

It is not hard to do so, this article will introduce 3 steps to complete that job.

# Preparation
Before going through the 3 steps, let's create a classic lambda project with SAM CLI:
```bash
sam init
```

After the above command, we will get a folder with these files:
```
├── README.md
├── events
│   └── event.json
├── hello-world
│   ├── app.js
│   ├── package.json
│   └── tests
│       └── unit
│           └── test-handler.js
└── template.yaml
```

Then we will start to shift this JS package into a TS package.

# Step 1: Add TypeScript Dependency
In the package.json, add the following codes:
```json
"scripts": {
  "compile": "tsc"
},
"devDependencies": {
  "aws-sdk": "^2.655.0",
  "@types/aws-lambda": "^8.10.51",
  "@types/node": "^13.13.5",
  "typescript": "^3.8.3"
}
```

- **scripts/compile**: this will be used to compile the TypeScript code to JavaScript
- **devDependencies**: since this is only for development, we do not need to add the packages to the dependencies block
- **aws-sdk**: depending on whether you are using AWS SDK in your lambda
- **@types/aws-lambda**: this is very vital for the code completion and typing checks
- **@types/node**: we need this package for built-in types
- **typescript**: where the tsc is from

# Step 2: Add tsconfig.json
```json
{
  "compilerOptions": {
    "module": "CommonJS",
    "target": "ES2017",
    "noImplicitAny": true,
    "preserveConstEnums": true,
    "outDir": "./built",
    "sourceMap": true
  },
  "include": ["src-ts/**/*"],
  "exclude": ["node_modules", "**/*.spec.ts"]
}
```
TypeScript compiler needs tsconfig.json to figure out how to transform TypeScript to JavaScript.
- **module**: CommonJS is fine here
- **target**: applying ES2017 will keep the async and await syntax instead of transforming them to Promise code. Since we are using Node12 as the runtime environment, Lambda function supports interpreting the syntax. Meanwhile, keeping async and await makes code clean and short
- **noImplicitAny**: recommended to have. Compiler will throw an error if there is a variable declared without type
- **preserveConstEnums**: more like a syntax sugar, but I would like to keep it on because it could keep enum classes in the JavaScript code in the form of object, which helps understand the JavaScript code
- **outDir**: any folder you would like to set as the compilation output
- **sourceMap**: this one is optional

# Step 3: Change the code
First, create the folder `src-ts`, and move `app.js` to that folder.
The `app.js` looks like this now:
```javascript
exports.lambdaHandler = async (event, context) => {
  const queries = JSON.stringify(event.queytStringParameters);
  return {
    statusCode: 200,
    body: `Queries: ${queries}`
  }
};
```
Let's create `app.ts` and replace it:
```typescript
import { 
  APIGatewayProxyEvent, 
  APIGatewayProxyResult 
} from "aws-lambda";
export const lambdaHandler = async (
  event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
  const queries = JSON.stringify(event.queryStringParameters);
  return {
    statusCode: 200,
    body: `Queries: ${queries}`
  }
}
```
Since this TypeScript file will be compiled to built folder, we also need to modify the Handler field in `template.yaml`, to ensure the lambda resource could locate the code in the right path:
```yaml
Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: hello-world/built
      Handler: app.lambdaHandler
```
We append the path built by the folder hello-world, so that AWS Lambda could find the handler correctly.
Now the directory looks like below:
```
├── README.md
├── hello-world
│   ├── built
│   │   ├── app.js
│   │   └── app.js.map
│   ├── package-lock.json
│   ├── package.json
│   ├── src-ts
│   │   ├── app.ts
│   │   └── tests
│   └── tsconfig.json
├── samconfig.toml
└── template.yaml
```
Deploy and Test
```bash
cd hello-world
npm install
npm run compile
cd ..
sam deploy --guided
```
After deployed successfully, we will see the Lambda function in AWS Console as follows:
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/87kc2t1exm30coomhhl2.png)

And we should be able to test the Lambda function using the following command:
```
▶ curl https://[API_ID].amazonaws.com/Prod/hello\?weather\=sunny
Queries: {"weather":"sunny"}
```

# Conclusion and Next Step
I find it really refreshing after utilizing TypeScript support for my Lambda functions. On one hand, it could save me tons of time looking up the API Doc for a method name or the parameter list. On the other hand, tsc also helps me to detect any potential problems before deploying.
In the next article, I will talk about how to do local integration tests on Lambda functions.