The Practical Client

Asynchronous Processing with TypeScript and Generic Promises

Promises make asynchronous processing simple, consistent and easy to use. And, with TypeScript and Promises.TypeScript providing support for generic Promises, you get both type safety and IntelliSense support.

While JavaScript itself is single-threaded, many JavaScript functions have supported asynchronous processing through callbacks (the variations on the jQuery AJAX function are just the most obvious example of functions with asynchronous support). As useful as callbacks are, though, there's no guarantee that the pattern used by one asynchronous function will be the same pattern used by some other asynchronous function.

Promises, as defined in the Promises/A+ specification on GitHub, provides through the Promise object a standard interface for working with asynchronous code. The Promise object represents your asynchronous code to the application by acting as a proxy to the application for an asynchronous process. The code using the Promise object can check the status of the process to see if it has completed successfully and also to retrieve the result of the process (when it becomes available). As an added convenience, you can call your asynchronous process at one point in your application, retrieve its Promise object and then pass that Promise object around your application to where you want to finally use the result of the process.

Using Promises not only provides a simple, consistent way to work with asynchronous processes, it also provides a number of guarantees. To begin with, once a Promise is fulfilled, it's immutable: A Promise will only succeed or fail once and will not change afterward. A Promise object also ensures you'll never miss the result of your asynchronous processing, even if the result is returned before you're ready for it. If you don't get around to specifying the processing you want to execute when the result is returned until after the result has been returned, it's all right. Your processing will be executed as soon as you attach it to the Promise object.

TypeScript and Promises
There are a number of libraries that support using Promises in TypeScript. I chose Promise.TypeScript (available through NuGet) for three reasons. First, Promise.TypeScript incorporates generics into its support for Promises, allowing me to get IntelliSense support for the result returned through the Promise object in addition to support for the Promise object itself. Second, Promise.TypeScript is itself written in TypeScript, making it easier (for me) to read. Third, the combination of the comments in the Promise.TypeScript file (Promise.ts) and the provided sample code are very helpful in getting started with Promises (you'll find the link to the sample code in the comments at the top of the Promise.ts file).

Before working with Promise.TypeScript, add this declaration to the top of your TypeScript file (the P prefix refers to the Promise.TypeScript module):

export interface Promise<Value> extends P.Promise<Value> { }

Wrapping Your Code
With that export added (and Promise.TypeScript added to your project through NuGet) you're ready to create a composable, asynchronous process (see "The HTML Page" at the end of this article for the HTML page that provided this code's environment). The first step is to put your code inside a method that returns a Promise object (which will wrap your code's result). This example defines a method called GetCustomer that returns a Promise wrapping a Customer object:

function GetCustomer( CustomerId: string ): Promise<Customer>
{

Within my method, I create a Deferred object that holds a Customer object by calling the Promise defer method (the Deferred object will generate the Promise object for you):

var deferredResult: P.Deferred<Customer>;
deferredResult = P.defer<Customer>();

Typically, your method will wrap some asynchronous operation. For this example, I'm using the jQuery asynchronous ajax method to retrieve a Customer object from a Web Service (using the ajax method means I need to use NuGet to add the DefinitelyTyped file for jQuery to my project). In the ajax method's success method I provide an arrow expression that the ajax method will call when the Customer object is retrieved successfully. The ajax method will pass the retrieved Customer object to the arrow expression.

To integrate the ajax method with Promises, all I need to do in the arrow expression is pass the Customer object to my Deferred object's resolve method. That will make the Customer object (when it appears) available through the Promise object I return from my GetCustomers method:

$.ajax( {
  url: "http://localhost:49306/CustomerManagement/" + CustomerId,
  dataType: "json",
  success: c =>
  {
    deferredResult.resolve( c );
  },

The ajax method error parameter also accepts another arrow expression. In this arrow expression, I'm interested in the second parameter passed by the ajax method: the error message. I use the message to assemble a Promises Rejection object, setting the Rejection object message property to the error message passed to my arrow expression. I pass that Rejection object to the Deferred object's reject method (that makes the Rejection object available through the Promise object):

error: ( e, errorMessage ) =>
  {
    var rej: P.Rejection;
    rej = { message: errorMessage };
    deferredResult.reject( rej );
  }
});

You must ensure your code either calls the Deferred object's reject method or resolve method, but not both.

The only thing left to do in my method is to have the Deferred object generate the Promise object and then return that Promise object to the code that called my method. To retrieve the Promise object, I call the Deferred object's promise method, as in this code:

  var promRes: Promise<Customer>;
  promRes = deferredResult.promise() 
  return promRes;
}

Using the Promise
Once you've written the method that returns the Promise object, you can just call your method and catch the Promise object:

var pCust: Promise<Customer>;
pCust = GetCustomer( "A123" );

Now that you've got the Promise object you can check its status and, when the Promise object is either rejected or resolved, retrieve the Promise's result (in this case, a Customer object) or error message.

In the following code, when the Promise object status is Rejected, I display the message from the Rejection object (available through the Promise object's error property). If the status is resolved, I retrieve the Customer object through the Promise object's result property:

var c: Customer
switch( pCust.status)
{
  case P.Status.Rejected:
    alert(pCust.error.message );
    break;
  case P.Status.Resolved:
    c = pCust.result;
    break;
}

However, that's actually the complicated way of working with the Promise object. It's much easier to use the Promise object's done and fail methods to do the same thing. To process the result of the Promise when the Promise is resolved you just need to pass an arrow expression to the done method of the Project object. The done method will pass the result of the Promise object to the arrow expression. Similarly, you can pass the Promise's fail method an arrow expression to process the Rejection object when the Promise is rejected. The fail method will pass the Promise object's Rejection object (with its error message) to the arrow expression.

The code in Listing 1 does what my previous switch-based code did by using done and fail. The code also uses the Promise's always method to update a div tag on the page when the Promise is fulfilled, regardless of whether the Promise is resolved or rejected (the always method isn't passed a parameter).

Listing 1: Using done, fail, and alert On a Promise Object

var c: Customer
GetCustomer( "A123" )
  .done( cRes => 
    {   
      c = cRes; 
    })
  .fail( r => 
    {       alert( r.message ); 
    })
  .always( () => 
    {  
      $("#messageDiv").val( "Customer retrieval complete"); 
    });

This is just the beginning, though. Next month, I'll move on to use Promises to compose chains of asynchronous processes and manage multiple parallel processes. Promise.

About the Author

Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube