Custom Types

The predefined types we've seen before are actually just simple functions, here is for instance the definition of enotype's color type:

exports.color = value => {
  if(!value.match(/^\s*#[0-9a-f]{3}([0-9a-f]{3})?\s*$/i))
    throw 'A color is required, for instance \'#B6D918\', \'#fff\' or \'#01b\'.';

  return value;
};

So as we see, all that's required for a custom type definition is a function that takes a single parameter - the value - and returns something!

Additionally, depending on the type, we might also include validation as demonstrated above - we simply throw a string message if there is a problem. When that happens the message is intercepted by enolib and embedded within a rich error message with metadata and a code snippet from the document.

const enolib = require('enolib');

const extraFoo = value => value + ' with extra foo!';

const document = enolib.parse('foo: bar');

document.field('foo').requiredValue(extraFoo);  // returns 'bar with extra foo!'

And just like that we are using our own type. Here's another example using the global registering magic from the previous chapter:

const enolib = require('enolib');

const fooOnly = value => {
  if(value !== 'foo')
    throw 'Only foo is accepted!';
  
  return value;
};

enolib.register({ fooOnly });

const document = enolib.parse('foo: bar');

document.field('foo').fooOnlyKey();  // returns 'foo'
document.field('foo').requiredFooOnlyValue();  // throws "Only foo is accepted!" with a code snippet

As a final note why it is sometimes preferable to use the less magicky function parameter type passing: On the one hand you might dynamically construct a type loader multiple times in your application when it depends on some local state (closure). On the other hand your type might not be a type per se but more of a conversion or modification, something like replaceNewlines or renderMarkdown, and in that case .requiredValue(renderMarkdown) looks a bit more sensible than .requiredRenderMarkdownValue().


Next page: Querying elements