BCHS Logo

BCHS

TypeScript, if you haven't heard of it, is a way of adding types to otherwise dynamically-typed JavaScript objects. This allows developers to statically check for type violations (number vs. string, unknown functions, nonexistence members, etc.) instead of resorting to painstaking run-time tests.

/* Variables: */
var foo; 
/* versus the typed... */
let foo: string; /* typed variable */

/* And functions: */
function bar(str, num) { return false; }
/* versus the typed... */
function bar(str: string, num: number): boolean { return false; }

/* JavaScript?  Ok.  TypeScript? No. */
bar(foo, foo);

Looks great, no? Well, don't get too excited: there are some… down-sides.

The tsc transpiler translates the TypeScript to JavaScript, making sure that types are consistent along the way. On OpenBSD, the tsc utility must be indirectly installed from npm. Let's grab it and make sure we also have our other tools.

# pkg_add openradtool node
# npm install typescript -g

I use -g as I'm usually building ports(7) of my software as a different user that also needs the tsc executable.

This brings us to downside #1: the node package manager. The least significant offense is that its installed components are not controlled by pkg_add(1) and it pollutes /usr/local/bin with symbolic links. So when you upgrade OpenBSD, you'll need to manually remove these links and reinstall the latest node packages. Furthermore, it's not clear to me whether npm install uses out-of-band version information, resulting in the same OpenBSD version with different TypeScript installations. Terrifying. A solution is to bring the TypeScript module into ports(7) itself, but I'm yet to roll up my sleeves for that one.

Anyway, TypeScript's typing offers more safety than plain JavaScript; and in the desert of front-end development, any water is sacred. Moreover, JavaScript and TypeScript can be mixed—the latter is just annotations to the former—so it's easy to edge TypeScript into your application without a re-write. For smaller systems, getting started with TypeScript usually involves more work with your Makefile than anything else.

In this article, I talk about the role of TypeScript in a BCHS ecosystem. Let's sail the full openradtool armada to see where it fits in, from configuration to client.

I will not talk about the JavaScript/TypeScript ecosystem of node modules and dependencies, grunting, gulping, and other willy wonka. We're going to keep it simple with some Makefiles.

openradtool
TypeScript chain from configuration to client.

But wait—why does openradtool deal with JavaScript at all? What does C have to do with JavaScript?

Fact is, most modern web applications interact with the server via JSON and need to do something with the data. And while your back-end might be beautifully chugging along, sandboxed and privilege separated, your front-end is probably a seething mess of unverified, untyped, unstructured code running on your poor clients' underpaid browsers. Since openradtool knows exactly about the structure of your data, it can create a well-typed, meaningful interface to the data.

getting started: javascript

Let's fake up a quick configuration config.ort for a client structure consisting of the client's name (name), their date of birth (dob), and the row identifier (id). We'll add an accessor function for fetching these records by unique identifier. All of this is documented in ort(5).

struct client {
  field name text;
  field dob epoch;
  field id int rowid;
  search id: name id;
};

I assume that your back-end application, via ort-c-source(1) and ort-c-header(1), is already configured to return JSON documents in response to requests. There's an example of this in the introduction to the openradtool series. Let's call the application app and have its default page respond to the client-id variable to return client record.

The plain JavaScript code generated by ort-javascript(1) provides classes and methods for formatting client records returned from your application. Let's put this into ort.js.

% ort-javascript config.ort >ort.js

How does this JavaScript code work? It takes JSON objects and fills in parts of the DOM tree as labelled by class attributes.

<DOCTYPE html>
<html lang="en">
  <head>
    <title>Example</title>
    <script src="ort.js"></script>
    <script src="index.js"></script>
  </head>
  <body>
    <div id="client">
      <span class="client-name-text"></span>:
      <input type="date" class="client-dob-date-text" />
    </div>
  </body>
</html>

This, according to the documentation, will fill the client's name as text under the element with client-name-text and the ISO-8601 date under client-dob-date-text. In our front-end code, we can fetch the client's JSON object with the following simplified code. (If you're using a framework, there probably exist other ways of doing this.) Let's assume your application is in /cgi-bin and put this in index.js.

function init() {
  var xmh = new XMLHttpRequest();
  xmh.onreadystatechange = function(){
    var v = xmh.responseText;
    if (xmh.readyState === 4 && xmh.status === 200) {
      var obj = JSON.parse(v);
      var e = document.getElementById('client');
      new ort.client(obj).fill(e);
    }
  };
  xmh.open('GET', '/cgi-bin/app?client-id=123', true);
  xmh.send();
}

That's it. The interface exported by ort-javascript(1) should give you enough tools to at least get started with your application. But first—let's make it run when the page loads by putting this in the same file.

window.onload = init;

In general, I'll bundle each HTML page's JavaScript requirements into one instead of using the magic bundling tools within the JavaScript ecosystem. So I'll concatenate my ort-javascript(1) output with a set of generic functions (such as the above for making the AJAX request) and per-page logic (invoking the server and fill methods).

index.dist.js: index.js ort.js
  cat index.js ort.js >$@

For the per-page logic, I'll use module patterns to make sure that all can be combined into a single file without namespace collisions. (This is also a good place to use jsmin, if it's installed.) This prioritises simplicity (one file per HTML page) over bandwidth considerations, where the client may be able to cache divisible components. Make it a point to use the module pattern to prevent concatenated functions from clobbering each other:

'use strict';
var index;
(function (index) {
  function init() {
    /* From above. */
  }
})(index || (index = {}));
window.addEventListener('load', index.init);

But wait… while our C code uses the best practises to be correct and guaranteed, how do we know that the generated JavaScript is correct? Or that we're correct in working with it? That's where TypeScript comes in handy.

typescript output

As of the last few versions (since version 0.5.0 or so), ort-javascript(1) can also output TypeScript. This TypeScript is functionally equivalent to the JavaScript: when compiled, it produces the same (or functionally similar) code.

If you want to use the TypeScript code, simply pass the -t flag and use the tsc utility to transpile your sources into JavaScript. There are many ways of doing this, depending upon how much one wants to use the npm environment. I personally don't use it at all: for each page, I compile a single JavaScript file from all of its dependencies.

index.dist.js: index.ts ort.ts
  tsc --strict --outFile $@ index.ts ort.ts

If I have an external JavaScript component I use (such as moment.js), I keep the latest stable code in a subdirectory, install it with my build components, and refer to its TypeScript .d.ts synopsis with the <reference path> construct. Let's return to our init method.

namespace index {
  export function init(): void {
    let xmh: XMLHttpRequest = new XMLHttpRequest();
    xmh.onreadystatechange = function(){
      let v: string = xmh.responseText;
      if (xmh.rekadyState === 4 && xmh.status === 200) {
        let obj: ort.clientData;
        let e: HTMLElement|null;
        obj = <ort.clientData>JSON.parse(v);
        e = document.getElementById('client');
        new ort.client(obj).fill(e);
      }
    };
    xmh.open('GET', '/cgi-bin/app?client-id=123', true);
    xmh.send();
  }
}
window.addEventListener('load', index.init);

There is one bothersome missing components in this sequence: we should have a wrapper safely converting from JSON.parse and our interfaces. I'll leave this in the area of future work.

We all agree that type safety is excellent. But now on to the bad news—yes, it's worse than the node package manager.

% time tsc --strict --outFile index.js index.ts
    0m02.97s real     0m04.58s user     0m01.13s system

Take a look at that again. Look above at our index function. Look again at that. That's 3 seconds. Seconds. That's a four-core machine with 4 GB RAM. 3. Seconds. I won't say anything more. I want to. But I won't.

Moral of the story? TypeScript is an excellent way to enforce a type contract between you and your code. And openradtool helps front-end developers by having a type-enforced API to its data kept in tune with the configuration. But using TypeScript instead of JavaScript incurs a steep compile-time performance hit.

documentation

The outputs of ort-c-source(1) and ort-c-header(1) are all well-documented, of course. The same goes with the JavaScript or TypeScript output of ort-javascript(1). It uses the jsdoc syntax, and strives to document all the tools and members available to front-end developers.

To generate documentation, you'll also need the jsdoc utility available via npm. See the disclaimer above about the node package manager, of course.

# npm install jsdoc -g

Assuming that we're built to handle TypeScript (jsdoc doesn't directly handle TypeScript), use the following. Of course, we can always just split the TypeScript to JavaScript generation into its own.

out: ort.ts
  tsc --outFile ort.ts.js ort.ts
  jsdoc -d $@ ort.ts.js
  touch $@
  rm -f ort.ts.js

The client JavaScript documentation for our examples is available as typescript-docs/index.html. I consider the documentation aspect of openradtool to be one of its most powerful features. If you can think of any way to improve this—let me know.

acknowledgements

I'd like to thank CAPEM Solutions, Inc., for funding this development and agreeing that it bests serves the community as open source.