What is Valuer?

Dima Parzhitsky
ITNEXT
Published in
7 min readOct 4, 2018

--

In my previous article I’ve briefly mentioned Valuer, which is a great tool to validate values before actually using them. Now I thought that it would be nice to fully introduce it, and also make a brief walk-through of its features.

Quickstart

Let’s start with an example. Suppose, you want to restrict some numbers to be natural. That’s classic. The task is to return the input if it fits the definition of natural numbers, otherwise throw an error describing what went wrong.

Usually you would do this kind of task using if .. else statements and return throw clauses:

if (typeof value !== "number" || isNaN(value))
throw new Error("value is not a number");
else if (value < 0)
throw new Error("value is a negative number");
else if (value % 1)
throw new Error("value is not an integer");
else return value;

I personally did this kind of thing all the time. This basically is a fine piece of code, but there are nevertheless some problems with it:

  • it is not self-descriptive; there is not a word about natural numbers here; you will probably have to add a comment or two, or better yet wrap it into a function with a good descriptive identifier;
  • the developers who read this code are forced to think in terms of implementation, instead of the terms of a problem;
  • developers, while using this approach, might or might not actually include the supplied value to the console output on errors, — you can only hope for the best;
  • when an error occurs, it is not immediately clear what it means: you see something like "0.3 is not an integer", but it doesn’t really help;
  • to keep console output clean and readable, you might want to standardize formulation of error messages, which is hard to achieve as it is usually far from the team’s primary task;
  • different developers might prefer different approaches to create the same condition, — e.g. use Number.isInteger instead of % operator, — which is not necessary a bad thing, but another wobbly wheel in a cart.

The good is the enemy of the best. Instead of saying ”value must be a primitive number not smaller than zero and divisible by one” what you actually want to express is ”value must be a natural number. Isn’t that so much simpler?

Yes, it is.

Now, check this out:

valuer(value).as(natural);

Valuer is all about validation

Let me show you this real quick

This natural identifier is an actual description of natural numbers.
Valuer embraces contract-based programming. To validate natural numbers, you have to first create a description of them for Valuer to understand what do they look like. Here’s what you might do:

import { Descriptor } from "@valuer/main/dist/types";const natural: Descriptor<number> = {
number: "integer",
spectrum: "non-negative",
};

This descriptor says that all of the following requirements has to be met for a value to be considered as valid natural number:

  • it has to be a primitive number¹ (implied by the "number" validator);
  • it has to be an integer;
  • it has to be greater than or equal to 0;

¹ It is considered a bad practice to use primitive wrappers, such as Number, String, Boolean or Symbol constructors. Valuer complies with that and requires those values to be primitive.

Now let’s use the given descriptor² to validate something:

import { valuer } from "@valuer/main";valuer(36.6, "body temperature").as(natural);

That 36.6 doesn’t really look like a natural number, — and Valuer knows that, because you’ve just provided that information in the descriptor!

² Alternatively, you can use a simpler syntax: .as("non-negative", "integer") .
Valuer will try to interpret the input and pick the befitting validators for you.

Depending on the failure³, an error will be thrown with a message like this:

Validation failed: body temperature is not an integer: value <number> 36.6

Et voilà! You not only know that there is something wrong, you know what is exactly, what does this mean at a higher level, and, presumably, how to fix it.

³ Remember that the order of validation usually depends on the order of keys in descriptor (a.k.a. validators), but that is not guaranteed by JavaScript itself.

An error message like that will always start with "Validation failed". It also will mention the role of the value (which in this case is "body temperature"), the failure ("is not an integer", you guessed it) and a string representation of the value, prepended by its type and the word “value.”

Error detalization is configurable. Less or more details could be provided.

Can I provide my own error messages?

Funny you should ask

Depending on what exactly is meant by the “error message”, you have at least two ways of providing them: via custom validators or using different mode.

Custom validators

Despite the variety of built-in validators in Valuer (which I will omit for now), you may have your own opinion on the things. Valuer appreciates that. You can create and use your own validators with your own conditions and failures:

const list: Descriptor = {
custom: Array.isArray,
};

This usage will make the built-in failure to appear on errors:

valuer("hi").as(list);
// Validation failed: value does not meet the constraint

Of course, the "custom" validator can be used only once, since objects in JavaScript cannot have more than one property with the same key.

To provide more meaningful error messages (which is apparently what you are after), just replace the key with the actual failure:

const MAX_REAL_AGE = 120;const age: Descriptor<number> = {
'is not a valid age': (n: number) => n > 0,
'is too big to be real': (n: number) => n <= MAX_REAL_AGE,
'shouldn't be used': (n: number) => n !== 13,
};
valuer(-5).as(age);
// Validation failed: value is not a valid age
valuer(256, "oldest man's age").as(age);
// Validation failed: oldest man's age is too big to be real
valuer(13, "baker's dozen").as(age);
// Validation failed: baker's dozen shouldn't be used

While using custom validators, you should remember that failure is the key and function returning boolean⁴ (a.k.a. thunk) is the value of any of them. Valid means “true”, so make sure that the function returns true if supplied with a valid value, and the failure is descriptive enough to otherwise help fix the error quickly.

Note that failure text should start with an auxiliary verb, like “is” or “doesn’t” etc. In this way the error message remains to be readable and consistent.

⁴ I recommend using elementary functions that check one thing at a time.

Different modes

Another way of showing custom errors is to perform completely different, customized logic whenever an error occurs. This approach implies that Valuer is used to just answer the simple question: “Was the input valid?”

Especially for that, there is a "check" mode. It alters the validation process and forces Valuer to just return a boolean instead of throwing an error:

const answer: Descriptor<string> = {
lengthRange: [ 100, Infinity ],
};
const valid: boolean = valuer("42").as(answer, { mode: "check" });if (!valid)
alert("Only when you know the question, will you know what the answer means");

In this example, next to the descriptor there is a config object⁵ with the .mode property equal to "check" — that is what switches the mode⁶.

⁵ Config object could be used only with descriptors. The alternative syntax from the second footnote (see²), accepts only descriptor values (a.k.a. validations).

This might be also useful if you aim to make smooth user experience in your web-project and/or don’t like reading the console too much.

⁶ In this example the mode is changed only for this execution. To change the mode once and for all, just use valuer.config method somewhere before the working line of code.
Use it responsibly though, because it becomes a pain to debug code with excessive usage of this feature. I recommend using it once per project. Or less.

So, that’s it?

Of course not! That would be pretty boring

This article covers the very basics of Valuer. It is now clear what Valuer is and what problems does it solve. But I omitted the part about “what can it do.”

What is not described here is:

  • exhaustive list of validators — "set", "kind", "pattern" and more;
  • other modes, like "assert", "describe" and "switch";
  • modifiers, including "details" and "rangeInclusive";
  • reusability — valuer.as(descriptor, config)(value, role);
  • descriptor abbreviations and other descriptive values;
  • creating real-world definitions — valuer.define(name, description);
  • validating non-primitive values;
import { valuer } from "@valuer/main";
import { Descriptor } from "@valuer/main/dist/types";
interface Person {
name: string;
age: number;
}
const nameDescriptor: Descriptor<string> = {
pattern: /(?:[A-Z][a-z]* ?){2}/,
lengthRange: [ 1, 32 ],
};
valuer.define<number>("age", {
typeOf: "number",
range: [ 1, 120 ],
});
valuer.define<Person>(":person", {
composite: {
name: nameDescriptor,
age: [ ":age" ],
},
});
const validate = valuer.as<Person>(":person");validate({ name: "hello", age: 17 }, "a person");
// Validation failed: 'name' in a person does not match the pattern
validate({ name: "Jon Snow", age: 0 }, "Jon Snow");
// Validation failed: 'age' in Jon Snow is out of bounds
validate({ name: "Jon Snow", age: 17 });
// (ok)

Oh, and did I mention that Valuer is written purely in TypeScript and it comes with rich IntelliSense support?

And by the way, there are other helpful related packages, like @valuer/help. All of them, including Valuer istelf, are licensed with the MIT license and covered with unit tests.

Valuer is constantly evolving. If you liked the concept behind it or especially if you feel like it could be better, join the team and check out the issues list to contribute.

See you there 👋

--

--