Integrate TypeScript with Twilio Functions and Twilio Programmable SMS

November 05, 2020
Written by
Jamie Corkhill
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by

typescript.png

Sometimes, when cost, resources, and time-to-market are factors, it can be worth considering going the serverless route. And, if a development team has decided upon a Node.js stack for the back-end, the benefits of TypeScript are enormous for safety, developer productivity, and new developer onboarding.

TypeScript is an extension of pure JavaScript - a “superset”, if you will - and adds static typing to the language. It enforces type safety, makes code easier to reason about, and permits the implementation of classic patterns in a more “traditional” manner. As a language extension, all JavaScript is valid TypeScript, and TypeScript is compiled down to JavaScript.

With Twilio Functions, you can easily create “serverless” applications that are natively integrated with the Twilio ecosystem. That means support for SMS messages, for example, is first-class. You won’t have to manage infrastructure or provision servers, and you can very quickly move from development to production.

In this tutorial, you’ll learn how to integrate Twilio Functions with TypeScript and send SMS messages via the Twilio Programmable SMS API.

Requirements

  • Node.js 10 - Consider using a tool like nvm to manage Node.js versions.
  • A Twilio Account with an SMS-capable phone number. If you are new to Twilio, you can create a free account and register a number.

Project Configuration

The Twilio CLI will permit you to manage Twilio resources from the command line. It can be extended by plugins, one of which is the Twilio Serverless Toolkit. The Serverless Toolkit adds CLI functionality to develop functions, among other things, locally and deploy them to the Twilio Runtime.

To begin, utilize the Twilio Serverless Toolkit via the Twilio CLI to scaffold a boilerplate. Install and authenticate with the required dependencies and plugins:

npm i -g twilio-cli
twilio plugins:install @twilio-labs/plugin-serverless
twilio-login

The twilio-login command will prompt you for your account credentials which can be found on your Twilio Console dashboard.

Next, bootstrap a boilerplate. In the command below, init will initialize a JavaScript project within a folder called ts-sms-demo (as specified) containing multiple stock/example files and directories.

twilio serverless:init ts-sms-demo
cd ts-sms-demo

In this post, by creating the project in this manner, you’ll end up retrofitting an existing JavaScript-based project to work with TypeScript (because the CLI creates a JavaScript project by default). If, instead, you want to start from scratch with TypeScript, you can swap out the init command above to: twilio serverless:init ts-sms-demo --typescript, and a TypeScript project will be bootstrapped.

You should now have a new directory with the name ts-sms-demo. After navigating inside, you’ll see a few default files and directories:

A view of the default boilerplate created by the Serverless Tooklit.

At this point, after running npm start, you can navigate your browser to http://localhost:3000 where the index page will walk you through the different files and folders. For this project, you’ll be most interested in the functions directory, but you’ll have to do a little configuration to achieve full interoperability with TypeScript.

Setting up TypeScript

This article covers converting a pre-initialized JavaScript project over to TypeScript. If you’re looking for a more in-depth resource regarding TypeScript configuration or moving a large JS project over to TS, take a look at Dominik Kundel’s article How to move your project to TypeScript - at your own pace.

You’ll install the TypeScript compiler and save it as a development dependency - it’s important to note that the tooling and functionality provided by TypeScript and the TypeScript compiler are available only at compile time - everything TypeScript specific is lost in the built JavaScript code post-compilation:

npm install typescript --save-dev

Using the installed TypeScript compiler, you’ll need to create a tsconfig.json file at the root of the project. The configuration parameters you specify within this file will affect the compilation process. Create the file by utilizing tsc - the compiler you installed above. From the root of the project, run the following command:

node_modules/.bin/tsc --init

Open the file, and clear its contents. We need to do a few things here - first, we’ll specify the compilation target and module system for the built code. Next, we’ll point the compiler to the root directory containing our TypeScript source code.

The compiler will take these files, compile them to JavaScript, and output them into an “out” directory, which we’ll also specify. Finally, as a method of “defensive coding”, we’ll enable “strict” mode, which specifies how strict the compiler should be when performing type checking.

  • target specifies which particular target of JavaScript should be emitted during compilation, and you can denote it via ECMAScript versions. For more information, visit the relevant section of the TypeScript Documentation.
  • module sets the module system, such as “CommonJS”, “UMD”, “AMD”, etc. The TypeScript documentation defines this in more detail here and here.

Make the following the new contents of your tsconfig.json file:

{
  "compilerOptions": {
    "target": "ES5",        
    "module": "commonjs",
    "outDir": "./build",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true
  }
}

The most important parts to us are outDir and rootDir. Relative to the root directory (the root project directory, that is), we’ve specified that we wish for the compiler to compile all TypeScript source files in the src folder and output the built JavaScript into another folder, also relative to root, called build. The compiler will preserve the directory structure from src within build. This is going to be important because we’ll need to specify to the Twilio CLI where our functions will live.

To test this, create a folder called src in your root directory. Inside of that folder, create another folder entitled functions, and place a TypeScript file inside:

mkdir src
mkdir src/functions
touch src/functions/hello-world.ts

Now, in your  terminal, run the TypeScript compiler:

node_modules/.bin/tsc

Within your project directory, you should see that a build folder has been created, and within that folder should be a one-to-one mapping of your src directories, except the files within have been compiled to JavaScript. I deleted the top-level assets and functions folders to make this easier to see, and you should do the same.

A view of the TypeScript source file and the output.


We can use the fact that the folder structure is preserved to specify to the Twilio CLI where the functions will live. We’ll write the function in TypeScript under src/functions, and then we’ll use the --functions-folder flag to state that the compiled functions will live under ./build/functions. Make the required changes to package.json now:


{
  "name": "ts-sms-demo",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "prestart": "tsc",
    "start": "twilio-run start --functions-folder ./build/functions",
    "predeploy": "tsc",
    "deploy": "twilio-run deploy --functions-folder ./build/functions"
  },
  "dependencies": {},
  "devDependencies": {
    "twilio-run": "^2.6.0",
    "typescript": "^4.0.5"
  },
  "engines": {
    "node": "10"
  }
}

Prefixing script names with pre or post, as in prestart and predeploy, creates a “lifecycle” around the script, if you will. In this case, when running npm start, the contents of prestart will be executed as well. The same goes for deploy and predeploy.

By default, the Twilio Runtime looks inside the root functions folder for functions. Since we’re using TypeScript, and since we may want to use other TypeScript files within our functions, we use the Serverless Toolkit’s --functions-folder flag to override the default behavior and specify a new location where the functions can be found. That’s the build/functions folder in our case.

Testing a TypeScript Function

It took a little bit of work, but you’re now ready to create your first TypeScript Function. If you haven’t already done so, go ahead and delete the top-level build, assets, and functions folders. We’ll be working within src, so navigate inside and create a functions folder (which you might already have from the example above).

There, create a file called hello-world.ts. We’ll test that you can build and deploy TypeScript functions by displaying “Hello, World!” to the client. Add the following function to hello-world.ts:

export const handler = (context, event, callback) => {
    return callback(null, 'Hello, World!');
}
  • context contains runtime specific properties, including a factory function that returns an initialized Twilio Client.
  • event contains properties pertaining to function invocations, such as a body or parameters passed up from an incoming HTTP Request.
  • callback returns execution control to the runtime - it accepts nullable error information and an optional response payload.

If you attempt to compile the project now with tsc command at the root of the project, you will immediately be met with errors - that’s because TypeScript does not know the expected types of context, event, and callback. This is a feature of “strict” mode, which we enabled earlier.

Although it may seem counterintuitive to explicitly state that we wish to have more possibilities for errors, these errors will help us to ensure that we are receiving the full benefits of TypeScript. We can rectify the issues either by explicitly typing the three parameters or explicitly providing handler with a type for a function signature.

Contrary to how most typing packages are usually installed (via Definitely Typed), types for Twilio Functions live within a twilio-labs package, which you can install as follows:

npm install --save-dev @twilio-labs/serverless-runtime-types

For the context parameter, the Context type is used, for callback, the ServerlessCallback type is used. These are both available as named exports from @twilio-labs/serverless-runtime-types/types.

The event parameter can be typed manually via an interface or type alias to contain the types that you expect as per the request parameters or POST body defined for the function.

In this case, rather than explicitly typing all three parameters, you’ll use ServerlessFunctionSignature to type handler, which will permit TypeScript to infer the parameters automatically as those types I mentioned above:

hl_lines="1 3"
import { ServerlessFunctionSignature } from '@twilio-labs/serverless-runtime-types/types';

export const handler: ServerlessFunctionSignature = (context, event, callback) => {
    return callback(null, 'Hello, World!');
}

At this point, you should be able to run npm start. TSC will compile your TypeScript source code and your function should be available via the Twilio Runtime on localhost. The name of the file specifies the location of the function in the URI, so in our case, the function is available at http://locahost:3000/hello-world.

Let’s make a GET Request to the function and ensure we get a “Hello, World!” response back:

curl http://localhost:3000/hello-world # -> Hello, World!

With that working, we can now integrate Twilio SMS.

Integrating Twilio Programmable SMS

If you don’t already have one, create a free Twilio account and purchase a trial number with SMS capabilities. New Twilio accounts are provided with a 15 USD free credit, so you can complete this tutorial without having to purchase any services. Ensure the phone number has SMS capabilities for your specific region, and if not, enable permissions here.

Using the context object, you can gain access to a pre-initialized Twilio client instance since the Twilio Runtime is aware of your Account SID and Auth Token.

Using this object, you can access the Twilio SMS REST API, which is wrapped by the client. You can go ahead and make the function asynchronous, and then you’ll send an SMS message, specifying a message body, your Twilio phone number, and a target (receiving) phone number:


import { ServerlessFunctionSignature } from '@twilio-labs/serverless-runtime-types/types';

export const handler: ServerlessFunctionSignature = async (context, event, callback) => {
    const twilioClient = context.getTwilioClient();

    try {
        await twilioClient.messages.create({
            body: 'Hello, World from Twilio Functions with TypeScript',
            to: '[Your Phone Number (Format: +11234567890)]',
            from: '[Your Twilio Phone Number (Format: +11234567890)]'
        });

        return callback(null);
    } catch (e) {
        return callback(e);
    }
}

You don’t necessarily have to await the promise settling result of sending the message, but since we are, we’ll wrap the operation in a try/catch block.

If the message is sent correctly, we’ll call the callback, passing null as the first parameter since we don’t have an error object. Otherwise, we’ll pass that error object along. Calling callback ends the request, so you can’t afford not to call it, otherwise, the request will hang.

If you have multiple functions, I’d recommend not repeating the try/catch block everywhere, and opting instead for some kind of withErrorHandling higher-order function that encapsulates your asynchronous and transient requests, watches for errors, and handles them accordingly. This would be considered more “DRY”.

If you run npm start and then make a GET Request to your function, as above (curl http://localhost:3000/hello-world), your target phone number will receive an SMS message after a short delay.

Deploying your Function

With the function developed and working locally, all you have to now is deploy it with:

npm run deploy

This will fire the predeploy hook to re-compile the project, and then the runtime will use the build/functions files as your functions. Your function URL will be of the form: https://ts-sms-demo-[identifier]-dev.twil.io/hello-world.

Conclusion

We’ve now gone through the process of making TypeScript compatible with Twilio Functions and integrated SMS. Consider taking the project a little further by researching how to make your function respond to POST requests and capture SMS messages from users.

Additionally, taking the following steps may be desirable:

  • Add the build/ directory to your .gitignore so that we can keep the compiled JavaScript out of any git repositories.
  • Consider taking approaches to consolidate error handling so you don’t have to constantly repeat try/catch in your functions.
  • Take a look at the Twilio Functions and Serverless Toolkit documentation.

Jamie is an 18-year-old software developer located in Texas. He has particular interests in enterprise architecture (DDD/CQRS/ES), writing elegant and testable code, and Physics and Mathematics. He is currently working on a startup in the business automation and tech education space, and when not behind a computer, he enjoys reading and learning.

  • Twitter: https://twitter.com/eithermonad
  • GitHub: https://github.com/JamieCorkhill
  • Personal Site: https://jamiecorkhill.com/
  • LinkedIn: https://www.linkedin.com/in/jamie-corkhill-aaab76153/