TypeScript (TS) has been increasing in popularity over the last couple of years. Did you know that you can use TypeScript with Lightning Web Components (LWC), and that there are several other Salesforce products and features that support TypeScript, such as Salesforce Functions, Lightning Web Runtime, and more?

In this first post of a two-part series, we’ll cover what TypeScript is, how you can install and use it in your projects, and the different data types that you can create with TypeScript. In the second post, we’ll cover the various places where you can use TypeScript within the Salesforce ecosystem.

What is TypeScript?

TypeScript is strongly-typed JavaScript (JS). It offers all of JavaScript’s features and an additional layer on top: TypeScript’s type system. TypeScript can’t run on its own; instead, TypeScript code complies to standards-based JavaScript.

How is it different from JavaScript?

JavaScript is a dynamically typed language, which means that it needs to be run to confirm that your business logic works as expected. For example, due to Type coercion, the output might be different from what you expect. TypeScript, on the other hand, offers features like static typing and a compiler that help you detect errors during compile time itself, which would otherwise pop up during runtime. This could include type errors, typos, usage of incorrect methods, uncalled functions, basic logic errors, non-exception failures, null and undefined checks, and so on. TypeScript also offers additional features, such as generics, interfaces, and enums, that encourage proper design and guide developers during development and code refactoring. We will cover some of these features later in this blog post.

Installing and compiling TypeScript

The TypeScript compiler is available as an npm package that can be installed in any of your projects. Once installed, you can start creating TypeScript files (*.ts). The compiler compiles the .ts files into standard .js files. It can be configured to target a specific version of ECMAScript (ES5, ES6, etc.). You will need to add a new script to the scripts section of the package.json file to use the compiler.

"scripts": {
    ...
    "ts-build": "tsc"
    ...
}

The tsconfig.json or jsconfig.json file can be used to control the behavior of TypeScript and the compiler in your project. Check out the documentation for various config options you can use.

During the compilation process, most of the type information is stripped away, with a few exceptions that we’ll discuss later in this blog post. Below is an example that shows the compiled JavaScript code for a given TypeScript code.

However, it is important to note that the TypeScript compiler does not modify the runtime behavior or logic of your JavaScript code.

Take this TypeScript code for example. It contains a printValue function that logs a value that is sent to it. The type of the val parameter is set to string. This code snippet throws a compile-time error at Line 6 because we are trying to assign a boolean value to a string.

However, once compiled, here is what the resulting JavaScript file looks like. There is no additional code generated that enforces the assignment of the val parameter to only string values. Hence, Line 6 would execute without any errors during run time.

You might be wondering: how did the JavaScript file get generated if there was a compile-time error? This is the default behavior of the compiler, which generates the JavaScript files even though there are type errors. You can set the --noEmitOnError flag (see docs) when running the compiler to prevent this from happening.

If you are using an editor with TypeScript plugins like VS Code, you needn’t always compile your files to find type errors. VS Code comes with built-in TypeScript support via the TypeScript language service, which shows suggestions and highlights errors as you finish typing. This is majorly helpful when trying to gradually adopt TypeScript into your projects.

Varying levels of strictness

Existing JavaScript projects can be incrementally migrated to TypeScript, i.e., you needn’t convert all your *.js files to *.ts at once. But you can still take advantage of TypeScript’s type system in your existing JavaScript projects. Below are the five stages in which you can adopt TypeScript, and each stage makes the type verification more strict.

In the first three stages, you will be working on JavaScript files themselves, but you can still use the TypeScript compiler or VS Code’s TypeScript language service to find errors. However, type checking works a bit differently in JS files. Check out this guide to know more.

To enable type checking on a few JavaScript files, add //@ts-check at the beginning of the files that you want to check. If you want to check all the files in the project, you can add the properties checkJs and allowJs to either the tsconfig.json file or pass them as flags to the compiler.

In the first two stages, TypeScript tries to infer data types based on your usage and point errors. If you are using VS Code, you can see the errors in the Problems tab even without running the compiler.

In the next stage, you can enforce types explicitly by specifying the data types of your variables via JSDoc constructs.

// The below JS Docs construct indicates that the variable is of type number.

/** @type {number} */ let a = 2; a = 'value'; // Error: Type string is not assignable to number

Once you are comfortable, you can start creating TypeScript files and take complete advantage of the type system.

Working with TypeScript

While the official TypeScript documentation provides a fairly detailed explanation, here is a quick summary of a few important concepts.

Types and interfaces

You can assign any of the standard data types to variables, input and output values for functions, etc. You can also use the special type any whenever you don’t want a particular value to cause type-checking errors.

// Declaring a variable of type "number".
let count: number;

// A function that takes in a "recordId" parameter of type "string" // and returns a value of type "boolean" function isValidRecordId(recordId: string): boolean { return true; }

You can even combine two or more types and create a union type. Here is a simple example:

// A function that returns a value of either "string" or "number" types
function getRecordId(name: string): string | number {
    return 'someid';
}

You can create a type alias to reuse a type definition in multiple places.

// A type alias "ID" to store a union type
type ID = string | number;

// Declaring a variable of type alias "ID"
let someId: ID;

// A function that returns a value of type alias "ID"
function getRecordId(name: string): ID {
    return 'someid';
}

You can also define types for non-primitives like objects. There are multiple ways to define object types (i.e, the shape of an object). First, you can define them inline.

// The structure of the "acc" param is defined inline
function getAccountName(acc: {id: string, name: string, createdDate: Date}): string{
    return acc.name;
}

Second, you can create a type alias.

// A Type Alias called Account that stores the object shape
type Account = {
    id: string
    name: string
    createdDate: Date
}

function getAccountName(acc: Account): string {
    ...
}

Finally, you can create an interface. The only difference between a type alias and an interface is that you can add new properties to an interface but not to a type alias.

// An interface called Account that stores the object shape
interface Account {
    id: string
    name: string
    createdDate: Date
}

// You can add new properties to an interface
interface Account {
    lastModifiedDate: Date
}

function getAccountName(acc: Account): string {
    ...
}

Generics

It is common that you create functions or classes that can work with different data types, but using the any type for those will not guarantee type safety. That’s where generics come into the picture. Generics allows you to pass types as arguments when calling a function or initializing a class, along with its values.

In the below example, we create a function query that takes in a SOQL query as a parameter and returns the results. The type of the results is obviously based on the object we are querying. So, here is how generics can be used to make sure the query function is type-safe.

// First we define the different types the query function supports
type Account {
    Id: string
    Name: string
}

type Contact {
    Id: string
    FirstName: string
    LastName: string
}

// Next, we define the query function.
// We use the "Type", a special kind of variable that works on types rather than values
// The value to this variable is passed when the function is being invoked
function query<Type>(soqlQuery: string): Type[] {
  ...
  return results;
}

// Finally, we call the query function
// We pass the Type information using angular braces < >
// accountResults is of Type "Account"
let accountResults = query<Account>("select Id, Name from Account");

// contactResults is of Type "Contact"
let contactResults = query<Contact>("select Id, FirstName, LastName from Contact");

You can see generics in action when you are building with Salesforce CLI plug-ins; we will cover this in Part 2 of this series.

Enums

Enums is one of the features offered by TypeScript that isn’t just for type safety. Similar to Apex, enums allow a developer to define a set of named constants. You can either initialize the constants to a particular value, or they default to auto-incremented numbers. Unlike the other type definitions, enums aren’t removed from the final JavaScript code during compilation, and they act like objects at runtime. const enums on the other hand, are completely removed during compilation.

// Defining an Enum
// Since all constants are uninitialized, they default to auto-incremented numbers
enum Status{
    Success,
    Failure
}

console.log(Status.Success); // Prints 0
console.log(Status.Failure); // Prints 1

// Defining another Enum, where we initialize the values
enum AccessLevel{
  ReadOnly = "ReadOnly",
  ReadWrite = "ReadWrite",
}

console.log(AccessLevel.ReadOnly); // Prints "ReadOnly"

Modules

The concept of modules is the same in both JavaScript and TypeScript. In TypeScript, you can additionally export and import type declarations like aliases and interfaces.

// shared.ts file
  
// A type declaration marked with "export" so that it can be imported in another file
export interface Contact {
    id: string,
    FirstName?: string,
    LastName: string
}

...

// A function marked with "export" so that it can be imported in another file
export function sayHello(obj: Contact | Lead){
    return `Hello ${obj.FirstName} ${obj.LastName}`;
}
// demo.ts file

// Importing the exported function and type declaration (interface)
import { Contact, sayHello } from './shared';

// Using the imported interface
let con: Contact = {id: '1', FirstName: 'John', LastName: 'Doe'};

// Using the imported function
console.log(sayHello(con));

TypeScript also allows you to declare Ambient Modules to specify the shapes of libraries not written in TypeScript. For example, here is a library (sf-libs) written in JavaScript, that includes a few functions.

// sf-libs.js
function convertTo15Digit(id){
    return id.substr(0,15);
} 
...
export {convertTo15Digit};

When you import this library in a TypeScript file, you would get a type error. This is because, no type information is available for the module and its functions.

import { convertTo15Digit, ... } from "sf-libs";
// Error: Cannot find module "sf-libs" or its corresponding type declarations.

Here is a TypeScript ambient module for the sf-libs JavaScript module. It is important to remember that ambient modules just define the shape of the module, and don’t contain any implementation details.

// sf-libs.d.ts
declare module "sf-libs" {
    function convertTo15Digit(id: string): string;
    ...
}

Checkout the ambient module declaration for the Lightning Web Components core module.

Summary

This post was just a glimpse into how you can use and gradually adopt TypeScript into your projects. You can dive deep into TypeScript using the below resources:

In the second post in this series, we will go over the various places where you can use TypeScript within the Salesforce ecosystem.

About the author

Aditya Naag Topalli is a 14x Certified Lead Developer Advocate at Salesforce. He empowers and inspires developers in and outside the Salesforce ecosystem through his videos, webinars, blog posts, and open source contributions, and he also frequently speaks at conferences and events all around the world. Follow him on Twitter or LinkedIn and check out his contributions on GitHub.

Get the latest Salesforce Developer blog posts and podcast episodes via Slack or RSS.

Add to Slack Subscribe to RSS