In this second post in our two-part series on TypeScript, we will discuss the various places that you can use TypeScript within the Salesforce ecosystem.

We strongly recommend that you read the previous post (Part 1), before reading this one. In it, we explain what TypeScript is and how is it different from JavaScript. We cover how you can gradually adopt TypeScript into your projects by choosing the level of type verification strictness that you need. We also discuss how VS Code has an inbuilt TypeScript language service that shows suggestions and highlights errors in your code as you finish typing. Finally, we provide a glimpse into various TypeScript concepts, such as types and interfaces, generics, enums, and modules.

Now that you have a basic understanding of how TypeScript works, let’s see where and how you can use TypeScript with Salesforce.

Lightning Web Components

Currently, Lightning Web Components (LWC) has limited support for TypeScript. It is not possible to do type checking between the JavaScript file and HTML template or to do type checking between components. For example, there is no way to ensure that the property types passed from a parent component will match the expected type in the child component. Also, TypeScript files (*.ts) present in an LWC bundle that’s deployed to the Salesforce Platform can not be processed by the LWC platform compiler.

However, you can take full advantage of VS Code’s TypeScript language service and use the lesser strict levels of TypeScript as described in the previous post. You can even leverage TypeScript declaration files to declare types that can be used across different components. Since the language service just provides static analysis, you needn’t worry about compiling or adding additional steps to your deployment.

To get started, you first need to specify the types using JSDoc constructs. Then, add the flag //@ts-check at the beginning of the JavaScript file to enable type checking. VS Code will now highlight type errors as shown in the screenshot below.

As a best practice, enable type checking for all of your LWC components at once by adding the checkJs flag to the compilerOptions in the jsconfig.json file present in your SFDX project’s lwc folder. Also, make sure you add ESNext as a target in the compiler options (the default if unspecified is ES3). This will prevent errors from showing up on ES5+ features like get/set accessors.

"compilerOptions": {
    "experimentalDecorators": true,
    "checkJs": true,
   "target": "ESNext"
}
...

With the checkJs flag added, you don’t need to add //@ts-check to each file anymore. Instead, to skip type checking on specific files, add the //@ts-nocheck flag at the beginning of those files.

When working with Salesforce data, you can define types for each object you work with. In case you are wondering about the object definitions in the .sfdx/typings/lwc/sobjects/ folder, those are type declarations for individual fields you import via @salesforce/schema/<objecname>.<fieldname>. These are automatically created by the Salesforce Extension Pack. While it defines the type of individual fields, it doesn’t hold the definition of the object itself.

So here is how you define an object type, for example, a Contact.

/**
 * @typedef Contact
 * @type {object}
 * @property {string} Id
 * @property {string} FirstName
 * @property {string} LastName
 * @property {boolean} isActive__c - your age.
 */

export default class Hello extends LightningElement {

    /** @type {Contact[]} */
    contacts;
    
}

VS Code will pick this up, enforce types, and show suggestions automatically. In the below GIF, you can see that when you type a dot(.) after contacts, it shows array methods like at, fill, every, etc. since contacts is an array. But when you type a dot(.) after contacts[0], it shows the properties of the Contact type that you defined earlier.

Types defined in one component can be imported inside another. Here is how a component HelloBinding would import the type Contact defined in the Hello component.

export default class HelloBinding extends LightningElement {

    /** @type {import('../Hello/Hello').Contact} */
    result;

}

To ensure that your custom type definitions are available across all your components without having to manually import them, you can create a TypeScript declaration file (*.d.ts) in the root of your project, and declare types using either type aliases or interfaces. For example, you can create a file mytypes.d.ts and create two types.

interface Contact {
    id: string,
    name: string
}

 interface Order {
    id: string,
    orderNumber: number
}

Next, you need to instruct TypeScript to also look at this file to find the type declarations. You can do this by adding the file’s location to the include property of the jsconfig.json file present in your SFDX project’s lwc folder.

"include": [
        "**/*",
        "../../../../.sfdx/typings/lwc/**/*.d.ts",
        "../../../../mytypes.d.ts"
],

You can now use these types in your LWC components using the JSDoc syntax, without having to import them.

/** @type { Contact } */
contacts;

The declaration file can be committed to your source control so that every developer on the project can use the same file.

The .sfdx/typings folder in your SFDX project also contains pre-defined type declarations for built-in modules like Lightning Message Service and Lightning Data Service adapters. Since this folder’s location is also present in the include property of the jsconfig.json, you can use these pre-defined types in your components. For example, the result of a getRecord operation has a certain shape, i.e., has pre-defined properties that don’t change based on the object you are querying. You can use VS Code’s suggestions to infer and import such types.

If you want stricter type checking, or want to author your components using .ts files, you need to first run the TypeScript compiler before deploying your components to the Salesforce Platform. One option is to create a predeploy Salesforce CLI hook that runs the tsc command to compile your TypeScript files to JavaScript files.

Lightning Web Runtime

When using Lightning Web Runtime (LWR) to create single-page applications that run off-platform, you can choose to use the TypeScript variant of LWC during project creation.

This creates a project that uses TypeScript files (*.ts) for Lightning Web Components instead of JavaScript. LWR automatically compiles these files before sending them to the client (browser). If not already present, be sure that you create a tsconfig.json file at the root of the project.

Below is an example of a simple TypeScript LWC component that uses standard types and custom interfaces. While Line 17 works correctly at runtime, TypeScript treats it as an error, since we are trying to set a property that isn’t valid for that type.

Even here, you can create your own type declaration files to support your implementation. As a best practice, create a types folder in the root of your project, and place your *.d.ts files in this folder.

// my.d.ts
interface Contact{
    id: string,
    name: string
}

Next, add the path of the types folder to the include property of tsconfig.json.

{
    "include": ["src/**/*", "types/*"]
}

You can now use these custom types in your components.

export default class HelloWorldApp extends LightningElement {
    con: Contact;
}

Salesforce Functions

TypeScript is one of the supported languages for creating Salesforce Functions — just pass typescript to the -l flag when running the generate function command.

sf generate function -n myfunction -l typescript

This command creates a TypeScript function that is structured exactly like a typical TypeScript project with files like index.ts and tsconfig.json.

A TypeScript function additionally includes the Salesforce Function SDK for Node.js (sf-fx-sdk-nodejs) as a dependency in the package.json file. This SDK provides a bunch of different pre-defined types and interfaces that you can import and use in your code. Thanks to these pre-defined types, you know precisely the shape of a parameter, property, or return type, which makes working with Salesforce data a lot easier.

You can even create your own types or import types from a third-party library if you are using one. For example, we can install the node-fetch library (see docs) from npm and use the types defined by the library. In the below example, we are importing and using the ReferrerPolicy type from the library.

Also, check out this TypeScript function recipe that returns the Salesforce org information attached to the context.

The @salesforce/ts-types library

The @salesforce/ts-types library (see docs) provides a collection of commonly desired types and a collection of type-narrowing functions for writing concise type-guards. For example, the AnyJson type can be used to hold any untyped JSON object, and different functions like hasBoolean, isBoolean, getBoolean can get the value of a specific type from the JSON object.

The use of this library also ensures runtime type safety. Here is an example:

import {AnyJson, asJsonArray, ensureJsonMap, hasBoolean, getBoolean } from "@salesforce/ts-types";

const response: AnyJson = [{
    start: 0,
    success: true,
    results: [{ name: 'first' }, { name: 'second' }]
}];

const results = asJsonArray(response, []);
results.forEach(item=>{
    let record = ensureJsonMap(item);
    if(hasBoolean(record, 'success')){
        let isSuccessful = getBoolean(record, 'success')
        // type of isSuccessful is boolean
    }
});

Salesforce CLI plug-ins

Built on top of OCLIF, a Salesforce CLI plug-in is created using TypeScript. This plugin also uses the @salesforce/ts-types library mentioned earlier. You can also see the concept of generics in action when using the query function from the JSforce library. In the below example, you can see that a custom interface Organization has been created, and is explicitly set as the type to the generic query function, which automatically sets the type of result to Organization.

public async run(): Promise<AnyJson> {
    ...
    const query = 'Select Name, TrialExpirationDate from Organization';

    // The type we are querying for
    interface Organization {
      Name: string;
      TrialExpirationDate: string;
    }

    // Query the org
    const result = await conn.query<Organization>(query);
    ...
}

Slack apps

One of the many ways to build Slack apps is to use Node.js with the @slack/bolt library added as a dependency. You can easily add typescript as an additional dependency and start using TypeScript to build Slack apps. Here is a sample Bolt app that is built using TypeScript. Similar to the Salesforce Function SDK for Node.js, the Bolt library also comes with a bunch of pre-defined types like SlashCommand, SlackEvent, MessageEvent, BlockAction, and many many more. The documentation for using TypeScript with Slack is a work in progress, so keep an eye out for updates.

Summary

The opportunities to use TypeScript with varying levels of strictness across different Salesforce products is gradually increasing, and hopefully, this blog post has given you a good glimpse into them. Now that you have seen how TypeScript can help you write robust code that will result in fewer runtime errors, how about giving it a try?

Here are some resources that will help you along the way:

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