The easiest way to convert and maintain Amplify functions in TypeScript
First off… I really wish the AWS Amplify CLI offered TypeScript as a language option when adding new functions. If they did, this article wouldn’t exist.
Since this isn’t an option at the moment, the only alternative is to initialize the function with a Node.js runtime, and then manually convert each function to TypeScript. It’s not particularly painful, but it does require following a few extra steps each time you create a new function.
After creating a few lambdas and converting them to TypeScript, I finally found a setup that is working well. Here’s how I’m maintaining my Amplify Lambdas in TypeScript:
1. Create a new lambda function to test
It’s easiest to create this TS setup with a new lambda function. You can convert an existing one if you prefer, but it’s easier to start with a relatively clean slate and minimal impact.
From the repo directory, initialize a new function by running amplify add function. Select the following minimalistic configuration:
? Select which capability you want to add: Lambda function (serverless function)
? Provide an AWS Lambda function name: ilovets
? Choose the runtime that you want to use: NodeJS
? Choose the function template that you want to use: Hello World
? Do you want to configure advanced settings? No
? Do you want to edit the local lambda function now? Yes
You should now have a lambda function that looks similar to the following:
2. Install TS as a dev dependency
If you already have TS installed within the repo, skip this step. If your repo doesn’t already have TS installed, make sure to install it as a dev dependency with one of the following commands respective to your package manager
NPM: npm install typescript --save-dev
Yarn: yarn add typescript --dev
PNPM: add typescript -D
3. Create a base tsconfig.json to share for all Lambdas
You could create a TSConfig per lambda, but it’s much easier to maintain if you create a single shared TSConfig and then extend it per Lambda. Inside of amplify/backend/function add a new file called tsconfig.json and populate the file with the following code:
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Amplify Lambda",
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"lib": ["dom", "esnext"],
"module": "commonjs",
"moduleResolution": "node",
"skipLibCheck": true,
"resolveJsonModule": true
}
}
It’s important to select commonjs for the module . There might be a way to set it up with a different module, but I ran into problems with any selection I made other than commonjs . The lambda wouldn’t run in AWS and complained with “Cannot find module ‘index'” .
4. Add tsconfig.json to your function
Inside of amplify/backend/function/ilovets add a new file called tsconfig.json and populate the file with the following code:
{
"extends": "../tsconfig",
"compilerOptions": {
"outDir": "./src"
},
"include": ["lib"]
}
This second TS file extends the base config and only specifies the input and output directory (we need this in each lambda). Now, each time you create a new lambda, you can extend the base TSConfig like so and maintain a consistent TS configuration across all functions.
5. Convert the JS function to TS
Inside of amplify/backend/function/ilovets delete index.js . We will be creating the TypeScript version of this file. Create a new folder inside of ilovets called lib . Populate it with a single file called index.ts . Add the following code to the file:
import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
export const handler = async (
event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
console.log(`EVENT: ${JSON.stringify(event)}`);
return {
statusCode: 200,
// Uncomment below to enable CORS requests
// headers: {
// "Access-Control-Allow-Origin": "*",
// "Access-Control-Allow-Headers": "*"
// },
body: JSON.stringify("Hello from Lambda!"),
};
};
(For this exercise we are typing out the lambda with API Gateway events, but you can change this to the types that support the invocation of the function for your use case.)
6. Add a script to generate types as part of the build
Inside the root repo’s package.json , add the following to scripts :
"scripts": {
...
"amplify:ilovets": "cd amplify/backend/function/ilovets && tsc -p ./tsconfig.json && cd -"
}
Now every time you run amplify push , TS will run this command prior to pushing your function. It will cd into your function and compile your TS files into JS before deployment.
7. (suggested) Gitignore all JS files within Lambda src
Totally preferential, but I like to ignore all JS files within the lambda src . They are now generated outputs and not manually maintained. Build outputs that are committed will just result in merge conflicts, clutter PR reviews, and add extra files to commit each push. Inside your .gitignore add the following to ignore all generated JS files.
amplify/backend/function/**/src/**/*.js
Testing the function
From the command line, manually invoke your TS compilation step to generate your JS from TS.
(npm/yarn/pnpm) run amplify:ilovets
If you set this up correctly, you should be able to look in your ilovets/src directory and find the TS compiled index.js .
Now anytime you run amplify push function ilovets , it will compile your TS into JS, and deploy only the JS to the AWS Lambda. This is a pretty detailed setup, but it’s truly only a few simple steps.
Supporting more lambdas
Now, each time you add a new lambda, you only need to follow steps 4, 5, and 6. You need to add a tsconfig to the function, add a lib folder with your TS, and add an amplify TS compilation script to package.json. Pretty easy to maintain. Everything else is a one-time setup.
My biggest “gotcha”
Only ever adding TS files to the lib folder. Never add anything TS to the src folder. For the longest time, I kept wanting to add my TS inside of the src directory. It felt wrong to keep it elsewhere. After a lot of investigation, I realized that this is not possible with Amplify, and that anything that ends up in your src directory will be packaged and deployed to Lambda. Keeping your TS in a separate folder prevents this. It might not feel the best, but it’s the price you pay for buying into Amplify.
The src folder is what is packaged and shipped to each Lambda. Lambdas don’t understand TS, so you must convert your TS to JS prior to each deployment. Adding TS files in srcwill do nothing more than bloat the build you are deploying to the lambda. We want to add all TS to thelib directory so that we can run tsc and generate the JS version, then deploy that to Lambda.
This is how I’m maintaining TS Lambdas in Amplify. If there is a better way to do this, I’d love to know! Share your experience in the comments and lets start a discussion.
Converting Amplify Lambdas to TypeScript was originally published in Better Programming on Medium, where people are continuing the conversation by highlighting and responding to this story.