28 January 2013

Using TypeScript to Define JSON Data

JSON has gradually been wearing away at XML's position as the primary format for data communication on the Web. In some ways, that's a good thing: JSON is much more compact and readable. In other ways, it's not so great: JSON lacks some of XML's features.

One of these features is document type definitions. For XML, there are a variety of formats (DTD, XML Schema, RELAX NG, etc.) for specifying exactly what your XML data looks like: what are the tag names, possible attributes, etc. JSON is a lot more loosey-goosey here.

Okay, that's not entirely true: there is JSON Schema. I've never known anyone to use it, but it's there. It's awfully verbose, though. (So are the definitional formats for XML, but it's XML  you expect it!)

I was thinking about this the other day, and I realized that there is actually a great definitional format for JSON already in existence: TypeScript! If you haven't heard of it, TypeScript is a superset of JavaScript which introduces optional strict typing. And since JSON is a subset of JavaScript, TypeScript is applicable to JSON as well.

One of the great features of TypeScript is that interface implementation is implicit. In Java or ActionScript, you have to specifically say that a type "implements MyInterface". In TypeScript, if it fits, it fits. For example:

interface List
{
    length: number;
}

function isEmpty(list: List): bool
{
    return list.length === 0;
}

console.log(isEmpty("")); // true
console.log(isEmpty("foo")); // false
console.log(isEmpty({ length: 0 })); // true
console.log(isEmpty({ length: 3 })); // false
console.log(isEmpty({ size: 1})); // Compiler error!

(Note: for some reason that I can't fathom, isEmpty() doesn't work on arrays. Well, TypeScript is still in development  version 0.8.2 right now. Update: I filed this as a bug.)

Note that you can use interfaces even on plain objects. So of course you can use it to describe a JSON format. Here's an example from a project I hope to release before too long:

interface Model
{
    uid: string;
}

interface Name extends Model
{
    citationStart?: number;
    html?: string;
    namebankID?: string;
    root?: bool;
    string?: string;
    type?: string;
    uri?: string;
    votes?: number;
}

interface Taxon
{
    canonicalName?: Name;
    illustrated?: bool;
    names?: Name[];
}

Now, for example, I can declare that an API search method will return data as an array of Taxon objects (Taxon[]). And look how compact and readable it is!

Note that there is one drawback here: there is no way to enforce this at run-time. JSON Schema might be a better choice if that's what you need. But for compile-time checking and documentation, it's a pretty great tool.

2 comments:

  1. I would use the following for the List interface:

    interface List {
    [index: number]: any;
    length: number;
    }

    ReplyDelete
    Replies
    1. That actually won't work in this example. For one, the plain object doesn't implement the subscript operator. For another, strings are not guaranteed to implement it in the ECMAScript standard. Both cases yield compile errors (try it in the TypeScript Playground).

      This, however, will work:

      interface List
      {
      [index: string]: any;
      length: number;
      }

      Basically, any object in JavaScript is considered to implement { [index: string]: any }.

      Delete