Type Schmecking - Hacking Thy Fearful Symmetry

Hacking Thy Fearful Symmetry

Hacker, hacker coding bright

Type Schmecking

created: May 12, 2017

These days, my pilgrimage through the divided kingdoms of EcmaScript made me skirt the boundaries of Typescriptlandia. From my short expeditions within, I have to admit: Typescript is a nice superset of JavaScript. And its main feature, type checking, is sure a fresh breath of structure in an otherwise world of stick-them-in-the-object-and-let-the-consumers-sort-em-out free-for-all.

But...

after getting up to speed with all the newest bells and whistles of EcmasScript, I'm feeling a bit relunctant to turn my back from my newly coalesced understanding and dive into yet another dialect.

But...

type checking is sure a nice thing to have. While this programming pattern of sticking everything in unstructured objects makes for quick code churning, it also makes for stream-of-consciousness APIs that are wretched breeding grounds of typos and forever-diverging naming variations.

So I thought... hey, there is that JSON Schema thang I keep playing with. That's also validating data structures... Hmmm... What if-- I mean, how about-- Oh hell with it. You know what? Let's hop down that rabbit hole and see where its leads...

Checking with Schemas (hence the schmecking)

Unlike Typescript, I'm interested here about runtime type checking. (mostly because taking on compile-time checking would be aiming way above my grade).

To do that, it'd be nice to take regular function...

function passwordify( seed_word ) {
    while( seed_word.length 

...and label it with the checks we want to perform. Both for the functions arguments...

// args must be a string
function passwordify( seed_word ) {
    while( seed_word.length 

...and its return value.

// args must be a string
// return value is a string of at least 10 characters 
//      (and therefore unbreakable)
function passwordify( seed_word ) {
    while( seed_word.length 

Hmmm... Isn't there an EcmasScript upcoming feature dealing with labelling things?

...or was it marking?

...adorning?

// args must be a string
// return value is a string of at least 10 characters 
//      (and therefore unbreakable)
function passwordify( seed_word ) {
    while( seed_word.length 

Decorators!

@args([ 'string' ])
@returns({ type: 'string', minimumLength: 10 })
function passwordify( seed_word ) {
    while( seed_word.length 

Once I knew I wanted to use decorators, the rest was pretty much creating a nice wrapper around the JSON Schema validator module Ajv to make it happen. I won't bore you with the implementation details -- the proof of concept is on GitHub for your perusing/forking/stealing pleasure. Suffice to say that it works.

A simple example

Here is our example, with all the actual code. Note that we have to wrap the method in a class to be able to use the decorators.

import JsonSchemaValidator from './lib/json-schema-type-checking';

const { args, returns } = JsonSchemaValidator();

class Foo {

    @args({ items: { type: 'string' } })
    @returns({ type: 'string', minimumLength: 15 })
    static passwordify( seed_word ) {
        while( seed_word.length 

... and the output.

Not bad, eh?

$ babel-node example.js

s3cr3t1111
{ not: 'good' }

 args value failed validation { args: [ { not: 'good' } ],
  errors:
   [ { keyword: 'type',
       dataPath: '[0]',
       schemaPath: '#/items/type',
       params: [Object],
       message: 'should be string' } ],
  stack: [] } (in json-schema-type-checking.js:20)

 return value failed validation { value: { not: 'good' },
  errors:
   [ { keyword: 'type',
       dataPath: '',
       schemaPath: '#/type',
       params: [Object],
       message: 'should be string' } ],
  stack: [] } (in json-schema-type-checking.js:20)

Using the data to check the data? Check.

Since we are using JSON Schemas, and doing things at runtime, there are a few more goodies within hand's reach. One you might already have noticed: JSON schema validation can do simple checks on the values (min/max length or value and that sort of things). And thanks to Ajv's bleeding edge support of JSON Schema specs, it goes further: those checks can refer to the data itself.

Simple function where we want the second argument to be equal or greater than the first.

By the by, the 1/0 value for $data is a JSON pointer, basically meaning "take the 0th (i.e., first) children of my parent as the value".

import JsonSchemaValidator from './lib/json-schema-type-checking';

const { args, returns } = JsonSchemaValidator();

class Foo {

    @args({ items: [ 
        { type: 'number' }, 
        { type: 'number', minimum: { '$data': '1/0' } } 
    ] } )
    static avg(min,max) {
        return (min+max)/2;
    };

};

console.log( Foo.avg( 2, 8 ) );
console.log( Foo.avg( 9, 8 ) );



Bingo.

$ babel-node example.js

5
8.5

 args value failed validation { args: [ 9, 8 ],
  errors:
   [ { keyword: 'minimum',
       dataPath: '[1]',
       schemaPath: '#/items/1/minimum',
       params: [Object],
       message: 'should be >= 9' } ],
  stack: [] } (in json-schema-type-checking.js:20)

Forgot to provide a value? Can't see default in that

Thanks to another Ajv rad feature, we can have our validation populate default values as well.

How about having a default tip of 2 bucks?

(yeah, yeah, I could have used tip = 2 in the function signature. It's an example, work with me here)

import JsonSchemaValidator from './lib/json-schema-type-checking';

const { args, returns } = JsonSchemaValidator();

class Foo {

    @returns({
        type: 'object',
        properties: {
            amount: { type: 'number' },
            tax:    { type: 'number' },
            tip:    { type: 'number', default: 2.00 },
        }
    })
    static calculate_bill( amount, tip ) {
        let bill = { amount };
        bill.tax = amount * 0.10;
        if ( tip ) bill.tip = tip;
        return bill;
    }

};

console.log( Foo.calculate_bill( 10, 3 ) );
console.log( Foo.calculate_bill( 10 ) );

Done.

$ babel-node example.js
{ amount: 10, tax: 1, tip: 3 }
{ amount: 10, tax: 1, tip: 2 }

Going schemad with the power

So far we've used ad-hoc types, but we can also use full-fledged schemas. And since they are JSON schemas, they could be processed into documentation elsewhere.

(Oh yeah, writing a Vue-based JSON schema viewer is in my todo list. Did I mentioned that?)


import JsonSchemaValidator from './json-schema-validator';

const { args, returns } = JsonSchemaValidator({
  schema: {
    definitions: { 
      bill: {
        type: 'object',
        properties: {
          amount: { type: 'number', documentation: 'raw amount' },
          tax:    { type: 'number', documentation: 'sigh...' },
          tip:    { 
            type: 'number',
            default: 2.00,
            documentation: "gratuity, because we're not savages"
          },
        }
      }
    }
  },
});

class Foo {

    @returns('#bill')
    static calculate_bill( amount, tip ) {
        let bill = { amount };
        bill.tax = amount * 0.10;
        if ( tip ) bill.tip = tip;
        return bill;
    }

};

This ain't nowhere close to being over...

And that's just the tip of the iceberg. By default check violations are only logged as errors, but there is already a configurable callback that can be tweaked to do anything else, from nothing to dying violently. While it's not implemented right now, it would also be easy to add a flag that would turn all type checks into no-ops if our production code has a need for speed.

So yeah, expect to see more updates on this specific project in the future. Meanwhile, enjoy!

comments powered by Disqus

About the author

Yanick Champoux
Perl necrohacker , ACP writer, orchid lover. Slightly bonker all around.