Skip to content

TypeScript: promises

TypeScript

A few months ago, I wrote an entrance dedicated to Javascript Promises, but I didn’t write any line of code. In TypeScript you can also work with promises, but the best way is to develop an example to show what promises are about.

A Javascript example

Let’s create first a Javascript example, based on firing an event after 4 seconds once the page have been loaded (we want to be notified when finished), and 4 seconds later, a second event will be fired. This is the code:


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Manejando datos - Promises</title>
</head>
<body>

<h1>Manejando datos - Promises</h1>

<div class="completion">Waiting</div>
<script>
        function wait(ms) {
            return new Promise( function (resolve) {
                window.setTimeout(function() { resolve()}, ms)    })        };

        var milliseconds = 2000;
        wait(milliseconds).then(finish).then(finish2);

        function finish() {    var completion = document.querySelector('.completion');    completion.innerHTML = "Completado tras " + milliseconds + "ms.";
        };
        function finish2() { window.setTimeout(function() {     milliseconds *= 2;    finish();    }, milliseconds)        }
    </script>
</body>
</html>

when executing the code, you’ll see this secuence:

1 2 3
Esperando 2 segundos 4 segundos

At the begining, you can read waiting, later, the first promise is fired, and next, the second.

Promises and TypeScript 1.x

The use of promises with TypeScript (version 1.x, because on TypeScript 2.x is totally different) is a bit complex if you are using ES5, what I’m using, because the object Promise is not available.

If you rewrite the Javascript code and you prepare a TypeScript project, you can verify that Promise is an unknown object.

Next step is dive into Internet if I can find a solution. As I could imagine, the answer is easy by using a TypeScript Promise project: https://github.com/pragmatrix/Promise. Because I couldn’t get running as it was, I copied and fix it, and also, including the promises to my TypeScript template, so I built an interface, and a class (copying the code of the Promise project, of course).


/// <reference path="imipromise.ts" />


class PromiseI<Value> implements Promise<Value>
{
constructor(public deferred: DeferredI<Value>)
{ }

get status(): Status { return this.deferred.status; }
get result(): Value { return this.deferred.result; }
get error(): Rejection { return this.deferred.error; }

done(f: (v: Value) => void ): Promise<Value> {
this.deferred.done(f);
return this;
}

fail(f: (err: Rejection) => void ): Promise<Value> {
this.deferred.fail(f);
return this;
}

always(f: (v?: Value, err?: Rejection) => void ): Promise<Value> {
this.deferred.always(f);
return this;
}

then<T2>(f: (v: Value) => any): Promise<T2>
{
return this.deferred.then<any>(f);
}
}

class DeferredI<Value> implements Deferred<Value>{

private _resolved: (v: Value) => void = _ => { };
private _rejected: (err: Rejection) => void = _ => { };

private _status: Status = Status.Unfulfilled;
private _result: Value;
private _error: Rejection = { message: "" };
private _promise: Promise<Value>;

constructor() {
this._promise = new PromiseI<Value>(this);
}

promise(): Promise<Value> {
return this._promise;
}

get status(): Status {
return this._status;
}

get result(): Value {
if (this._status != Status.Resolved)
throw new Error("Promise: result not available");
return this._result;
}

get error(): Rejection {
if (this._status != Status.Rejected)
throw new Error("Promise: rejection reason not available");
return this._error;
}
defer<Value>(): Deferred<Value>
{
return new DeferredI<Value>();
}
then<Result>(f: (v: Value) => any): Promise<Result>
{
var d = this.defer<Result>();

this
.done(v =>
{
var promiseOrValue = f(v);

// todo: need to find another way to check if r is really of interface
// type Promise<any>, otherwise we would not support other
// implementations here.
if (promiseOrValue instanceof PromiseI)
{
var p = <Promise<Result>> promiseOrValue;
p.done(v2 => d.resolve(v2))
.fail(err => d.reject(err));
return p;
}

d.resolve(promiseOrValue);
} )
.fail(err => d.reject(err));

return d.promise();
}

done(f: (v: Value) => void ): Deferred<Value>
{
if (this.status === Status.Resolved) {
f(this._result);
return this;
}

if (this.status !== Status.Unfulfilled)
return this;

var prev = this._resolved;
this._resolved = v => { prev(v); f(v); }

return this;
}

fail(f: (err: Rejection) => void ): Deferred<Value>
{
if (this.status === Status.Rejected) {
f(this._error);
return this;
}

if (this.status !== Status.Unfulfilled)
return this;

var prev = this._rejected;
this._rejected = e => { prev(e); f(e); }

return this;
}

always(f: (v?: Value, err?: Rejection) => void ): Deferred<Value>
{
this
.done(v => f(v))
.fail(err => f(null, err));

return this;
}

resolve(result: Value) {
if (this._status !== Status.Unfulfilled)
throw new Error("tried to resolve a fulfilled promise");

this._result = result;
this._status = Status.Resolved;
this._resolved(result);

this.detach();
return this;
}

reject(err: Rejection) {
if (this._status !== Status.Unfulfilled)
throw new Error("tried to reject a fulfilled promise");

this._error = err;
this._status = Status.Rejected;
this._rejected(err);

this.detach();
return this;
}

private detach()
{
this._resolved = _ => { };
this._rejected = _ => { };
}
}

class IteratorI<E> implements Iterator<E>
{
current: E = undefined;

constructor(private f: () => Promise<E>)
{ }

advance() : Promise<boolean>
{
var res = this.f();
return res.then(value =>
{
if (isUndefined(value))
return false;

this.current = value;
return true;
} );
}
}

function isUndefined(v)    {    return typeof v === 'undefined';    }

class P  {
contructor() {}

iterator<E>(f: () => Promise<E>): Iterator<E>
{
return new IteratorI<E>(f);
}

generator<E>(g: () => () => Promise<E>): Generator<E>
{
return () => this.iterator<E>(g());
};
defer<Value>(): Deferred<Value>
{
return new DeferredI<Value>();
}

resolve<Value>(v: Value): Promise<Value>
{
return this.defer<Value>().resolve(v).promise();
}
reject<Value>(err: Rejection): Promise<Value>
{
return this.defer<Value>().reject(err).promise();
}
each<E>(gen: Generator<E>, f: (e: E) => void ): Promise<{}>
{
var d = this.defer();
this.eachCore(d, gen(), f);
return d.promise();
}
eachCore<E>(fin: Deferred<{}>, it: Iterator<E>, f: (e: E) => void ) : void
{
it.advance()
.done(hasValue =>
{
if (!hasValue)
{
fin.resolve({});
return;
}

f(it.current)
this.eachCore<E>(fin, it, f);
} )
.fail(err => fin.reject(err));
}

unfold<Seed, Element>(
unspool: (current: Seed) => { promise: Promise<Element>; next?: Seed },
seed: Seed)
: Promise<Element[]>
{
var d = this.defer<Element[]>();
var elements: Element[] = new Array<Element>();

this.unfoldCore<Seed, Element>(elements, d, unspool, seed)

return d.promise();
}
unfoldCore<Seed, Element>(
elements: Element[],
deferred: Deferred<Element[]>,
unspool: (current: Seed) => { promise: Promise<Element>; next?: Seed },
seed: Seed): void
{
var result = unspool(seed);
if (!result) {
deferred.resolve(elements);
return;
}

// fastpath: don't waste stack space if promise resolves immediately.

while (result.next && result.promise.status == Status.Resolved)
{
elements.push(result.promise.result);
result = unspool(result.next);
if (!result) {
deferred.resolve(elements);
return;
}
}

result.promise
.done(v =>
{
elements.push(v);
if (!result.next)
deferred.resolve(elements);
else
this.unfoldCore<Seed, Element>(elements, deferred, unspool, result.next);
} )
.fail(e =>
{
deferred.reject(e);
} );
}


when(...promises: Promise<any>[]): Promise<any[]>
{
var allDone = this.defer<any[]>();
if (!promises.length) {
allDone.resolve([]);
return allDone.promise();
}

var resolved = 0;
var results = [];

promises.forEach((p, i) => {
p
.done(v => {
results[i] = v;
++resolved;
if (resolved === promises.length && allDone.status !== Status.Rejected)
allDone.resolve(results);
} )
.fail(e => {
if (allDone.status !== Status.Rejected)
allDone.reject(new Error("when: one or more promises were rejected"));
} );
} );

return allDone.promise();
}
}

export = P;

At the begining of the code, there is a reference to interfaces used:


enum Status {
Unfulfilled,
Rejected,
Resolved
}

interface Rejection
{
message: string;
}
interface PromiseState<Value>
{
/// The current status of the promise.
status: Status;

/// If the promise got resolved, the result of the promise.
result?: Value;

/// If the promise got rejected, the rejection message.
error?: Rejection;
}
interface Promise<Value> extends PromiseState<Value>
{
/**
Returns a promise that represents a promise chain that consists of this
promise and the promise that is returned by the function provided.
The function receives the value of this promise as soon it is resolved.

If this promise fails, the function is never called and the returned promise
will also fail.
*/
then<T2>(f: (v: Value) => Promise<T2>): Promise<T2>;
then<T2>(f: (v: Value) => T2): Promise<T2>;

/// Add a handler that is called when the promise gets resolved.
done(f: (v: Value) => void ): Promise<Value>;
/// Add a handler that is called when the promise gets rejected.
fail(f: (err: Rejection) => void ): Promise<Value>;
/// Add a handler that is called when the promise gets fulfilled (either resolved or rejected).
always(f: (v?: Value, err?: Rejection) => void ): Promise<Value>;
}
interface Deferred<Value> extends PromiseState<Value>
{
/// Returns the encapsulated promise of this deferred instance.
/// The returned promise supports composition but removes the ability to resolve or reject
/// the promise.
promise(): Promise<Value>;

/// Resolve the promise.
resolve(result: Value): Deferred<Value>;
/// Reject the promise.
reject(err: Rejection): Deferred<Value>;

done(f: (v: Value) => void ): Deferred<Value>;
fail(f: (err: Rejection) => void ): Deferred<Value>;
always(f: (v?: Value, err?: Rejection) => void ): Deferred<Value>;
}

interface Generator<E>
{        (): Iterator<E>;    }

interface Iterator<E>
{
advance(): Promise<boolean>;
current: E;
}

as you can imagine, I need to modify the RequireJS config file, to add iPromise:


/// <reference path="../typings/require.d.ts" />

/**
* Application configuration declaration.
*/
require.config({

baseUrl: 'ts/',

paths: {
//main libraries
"jquery": '../js/jquery',
"bootstrap": '../js/bootstrap.min',
"imipromise": 'imipromise', // Promise Status Enum
//shortcut paths
templates: '../templates',

//require plugins
text: '../typings/require/text',
tpl: '../typings/require/tpl',
json: '../typings/require/json',
hbs: '../typings/require-handlebars-plugin/hbs'
},

shim: {
"jquery": {
exports: '$'
},
"bootstrap": {
deps: ['jquery']
},
"Handlebars": {
exports: 'Handlebars'
}
}
});

require(["app"]);

Also, prepare app.ts to use it as well:

/// <reference path="../typings/jquery.d.ts" />
/// <reference path="../typings/require.d.ts" />
/// <reference path="../typings/bootstrap.d.ts" />

/// <reference path="menu.ts" />
/// <amd-dependency path="menu.ts" />

require([
    'Menu',
    'jquery',
    'bootstrap',
    "imipromise" // Promise Status Enum
], function (MenuApp, $) {
    'use strict';
    $(function () {
        var menu = new MenuApp();
    });
});

And everything is ready to go. Now, it is turn for menu.ts to complete the example:

/// <reference path="../typings/jquery.d.ts" />
/// <reference path="../typings/bootstrap.d.ts" />
/// <reference path="mipromise.ts" />
/// <reference path="imipromise.ts" />

import $ = require("jquery");
import Promise = require("mipromise");

var P = new Promise();
// https://github.com/pragmatrix/Promise
var defer = P.defer;
var when = P.when;


var opciones_app = {
    base: 'div'
}

class MenuApp {
    private debug: boolean = false;
    
    private div_base: JQuery;
    
    constructor() {
        this.div_base = $(opciones_app.base);
        console.log('Plantilla cargada con exito!');
        this.inicializar()
    }
    inicializar() { // Aquí vienen REQUISITOS iniciales
        var that = this;
        var milliseconds = 4000;
        console.log('Iniciando promesa');
        this.div_base.html('Sin promesa!');
        this.wait(milliseconds)
            .then(() => that.promise_done());

    }
    promise_done() {
        console.log('Promesa cumplida!');
        this.div_base.html('Promesa cumplida!');
    }
    wait(ms) { 
        var d = defer<string>();
        this.div_base.html('¿se cumplirá la promesa?');
        window.setTimeout(() => { d.resolve("ok")} , ms);
        return d.promise();
    }
}

export = MenuApp;

And here you have the code in action:

Promesas en TypeScript

Promesas en TypeScript

The code is available on GitHub. IMPORTANT: This is valid if you work with TypeScript 1.x, because if you move to versión 2, more advanced, the Promise object is available so you don’t need to include extra plugins as I did on this example!

Happy day and happy coding!

Manejando Datos Newsletter

Noticias, entradas, eventos, cursos, … News, entrances, events, courses, …


Gracias por unirte a la newsletter. Thanks for joining the newsletter.