Object Model


Model Definition and Runtime Type Checking for JavaScript

Define typed models

   var User = new Model({
      name: String,
      female: Boolean,
      birth: Date
   });
Validate at runtime

   var joe = new User({
      name: "Joe",
      female: false,
      birth: 1986
   });
TypeError: expecting birth to be Date, got Number 1986

What is this library ?

A recurring criticism of JavaScript is that it is a weakly typed language. The lack of strong typing is still responsible for numerous bugs in web applications. As an attempt to get rid of these issues, new typed languages that compile to JavaScript have been invented, like TypeScript from Microsoft. We also have static analysis tools like Flow from Facebook. These solutions bring static typing, which means that it only validate your code at build time, not runtime. Once compiled in JavaScript and run in the browser, there is no longer any guarantee that the variables you are working with have the right type.

From my own experience, static typing is insufficient to prevent most of bugs caused by type errors. This is because as part of JavaScript applications, we rely heavily on unreliable data sources: user inputs, web services, server-side rendering, browser built-ins, external dependencies, CDN... Static typing can not check the validity of this content since it is retrieved at runtime. For the same reason, developers usually face more type errors with this kind of data compared to their code variables that are under their control.

That's why Object Model is about strong dynamic type checking: it aims to get this type validation at runtime without requiring a new language or a compilation step. Whenever a property of your model is modified, the whole model is validated against its typed definition. This allows you to identify a problem much more quickly thanks to the generated exceptions. Object Model is also very easy to master: no new language to learn, no new tools, no compilation step, just a minimalist and intuitive API in a micro-library.

Validating at runtime also brings other benefits : you can define your own types, and use them in complex model definitions with custom assertions and more specific tests that can even change depending on your application state. Actually it goes much further than just type safety. Go on and see for yourself.

What's inside the box ?

Many features, hopefully neither too much nor too few:

Which browsers are supported ?

This library has been unit tested on latest versions of Chrome, Firefox and Edge, on Internet Explorer 11, Opera 20, and Safari 5.1. It should work on any browser implementing ECMAScript 5 specifications. If you find a fairly recent browser on which the tests do not pass, please post a Github issue and I will try to add support for it.

Support for IE < 11 had to be dropped in v2 because it required many hacks and was holding back other browsers. The latest version supporting IE9-10 is v1.1.5.


Download

Note: You are currently on v1 documentation. The latest documentation is available here.

Latest 1.x version: v1.1.5

Changelog
v1.1
model.extend can now take models and/or models definitions as arguments
fixed some issues with object deep merging on model extensions
make array models keys enumerable
v1.0
added model.validate and model.test
moved conventions to overridable exposed functions
preserve object properties out of model definition at instanciation
aligned array models constructors with other models behaviour
v0.9
use of ECMAScript 6 proxies if available
performance improvements, especially on Array models
v0.8
cycle detection and validation of circular object structures
support for Safari 5
v0.7
API redesign to get a single namespace
UMD version
added model.assert
removed arrayModel.min/max
some performance improvements
v0.6
corrected models prototypal chain
models prototype is used to store default values and methods
automatic instanciation of Function models on object model properties
v0.5
constants and private properties by name convention
unit tests
v0.4
Model.defaults, Model.Function.defaults
Model.extend, Model.Array.extend
v0.3
Model.Function
arguments and return value validation
v0.2
Model.Array
Optional & mandatory parameters
Multi-types parameters
v0.1
basic object Model
null-safe traversing
validation by constructor or equality
Future versions

Possible changes under consideration:

  • Make Model API use function models itself (wow, so meta X_x)
  • Greater use of ES6 Proxies
  • ES6 only version
  • more detailed path information in object models type errors

GitHub repository

You can report bugs and contribute to the project on the GitHub repository.


Basic models

Model is the library namespace and the constructor function for Models. It takes as an argument a model definition, and returns a model constructor. Basic model constructor validate the argument passed against the model definition, and return the validated value.

Model
var NumberModel = Model(Number);
// 'new' keyword is always optional
Instance
var x = NumberModel("42");
TypeError: expecting Number, got String "42"

Object models

Object models validate nested object properties against a definition tree. You may consider them as definitions of classes with advanced validation options. They provide automatic validation at initial and future assignments of the properties of the instance objects.

Model
var Order = new Model({ // or Model.Object
	product: {
		name: String,
		quantity: Number,
	},
	orderDate: Date
});
Instance
var myOrder = new Order({
	product: { name: "Apple Pie", quantity: 1 },
	orderDate: new Date()
});

myOrder.product.quantity = 2; // no exceptions thrown
myOrder.product.quantity = false; //try to assign a Boolean
TypeError: expecting product.quantity to be Number, got Boolean false

Optional properties

By default, model properties are mandatory. That means all properties defined are required on instance declaration, otherwise an exception will be raised. But you can specify a property to be optional by using the bracket notation, borrowed from the JSDoc specification:

Model
var User = Model({
	email: String, // mandatory
	name: [String] // optional
});
Instance
var stan = User({ email: "stan@smith.com" }); // no exceptions
var roger = User({ name: "Roger" }); // email is mandatory
TypeError: expecting email to be String, got undefined

Multiple types

Several valid types can be specified for one property, aka union types. So optional properties are actually union types between the original type and the values undefined and null. To declare an optional union type, add undefined to the list.

Model
var Animation = new Model({
	delay: [Number, String], // can be a Number or a String
	easing: [Boolean, String, undefined]
	// optional property which can be a Boolean or a String
});
Instance
var opening = new Animation({ delay: 300 }); // easing is optional
opening.delay = "fast"; // String is a valid type
opening.delay = null;
TypeError: expecting delay to be Number or String, got null
opening.easing = true; // Boolean is a valid type
opening.easing = 1;
TypeError: expecting easing to be Boolean or String or undefined, got Number 1

Value checking and enumerations

Instead of types, values can be specified for model properties. The property value must match the model one. If a regular expression is passed, the value must match it. Use brackets notation for value enumerations. Note that values and types can be mixed for one property.

Model
var Shirt = new Model({
	// the only acceptable value is "clothes"
	category: "clothes",

	// valid values: 38, 42, "S", "M", "L", "XL", "XXL"...
	size: [Number, "M", /^X{0,2}[SL]$/],

	// valid values: "black", "#FF0000", undefined...
	color: ["black","white", new RegExp("^#([A-F0-9]{6})$"), undefined]
});

Null-safe object traversal

When you want to traverse nested objects, you always have to worry about the null pointer exception. Some languages such as Groovy have a safe navigation operator represented by ?. to safely navigate through potential null references. In JavaScript, there is no such solution so you have to manually check for undefined/null values at each level of the object. But within an Object Model, declared properties are null-safe for traversal: every instance complete its structure with undefined properties according to the model definition.

Model
var Config = new Model({
	local: {
		time: {
			format: ["12h","24h", undefined]
		}
	}
});

var config = { local: undefined }; // object duck typed
var model_config = Config(config); // object model
Traversal
if(config.local.time.format === "12h"){ hour %= 12; }
TypeError: Cannot read property 'time' of undefined

// so to prevent this exception, we have to check this way:
if(config != null
&& config.local != null
&& config.local.time != null
&& config.local.time.format === "12h"){
	hour %= 12;
}

// with object models, no worries :)
if(model_config.local.time.format === "12h"){ hour %= 12; }
// model_config.local.time.format returns undefined

Default values assignement

To specify default values for some properties of your model, put them in the model prototype. You can also use the defaults method as a shorthand for setting all the default values at once. If these are not defined at object instanciation, their default value will be assigned.

Model
var FileInfo = Model({
	name: String,
	size: [Number],
	creationDate: [Date],
	writable: Boolean
}).defaults({
	name: "Untitled file",
	size: 0,
	writable: true
});
Instance
var file = new FileInfo({ writable: false });
file.name; // name is mandatory but a default value was passed
"Untitled file"
file.size; // size is optional, but the default value still apply
0
file.creationDate; // no default value was passed for this property
undefined
file.writable; // passed value overrides default value
false
Object.keys(file);
["name","size","creationDate","writable"]

Composition with models as types

Models declared can also be used for type checking, so you can compose structures of objects. Note that the sub-models do not necessarily need to be instantiated through the model constructor to be considered valid: only the definition of the sub-model must be respected. This is called duck typing, which can be summarized as "If it looks like a duck and quacks like a duck, then it's a duck". This naive approach is much more flexible and allows you, for example, to parse composed objects from JSON in one step.

Model
var Person = Model({
	name: String,
	age: [Number]
});

var Lovers = Model({
	man: Person,
	woman: Person
});
Instance
var joe = { name: "Joe", age: 42 };
var ann = new Person({
	name: joe.name + "'s wife",
	age: joe.age - 5
});

var joe_and_ann = Lovers({
   man: joe,  // object duck typed
   woman: ann // object model
});

Inheritance by extension

Extensions create new models based on existing model definitions. You can declare new properties or modify previous ones. Therefore, it is an easy way to reproduce subtyping and class inheritance.

Model
var Person = Model({
	name: String,
	female: Boolean
});

var Mother = Person.extend({
	female: true,
	child: Person
});
Instance
var joe = new Person({ name: "Joe", female: false });
var ann = new Person({ name: "Joe's wife", female: true });
var joanna = new Person({ name: "Joe's daughter", female: true });

ann.child = joanna;
ann = Mother(ann); // cast ann to Mother model
Mother.test(ann) && Person.test(ann)
true
joe = Mother(joe); // try to cast joe to Mother model
TypeError: expecting female to be true, got Boolean false
expecting child to be {
	name: String,
	female: Boolean
}, got undefined

Multiple inheritance

But it goes further: you can do multiple inheritance and mix any number of parent models definitions and assertions. If some properties have the same name, those of the last object overrides the others.

Model
var Client = Person.extend(User, Order, { store: String });

Client.prototype.sendConfirmationMail = function(){
	return this.email + ": Dear " + this.name
	+ ", thank you for ordering "
	+ this.product.quantity + " " + this.product.name
	+ " on " + this.store;
};

Object.keys(Client.definition);
["name", "female", "email", "product", "orderDate", "store"]
Instance
var joe = new Client({
	name: "Joe",
	female: false,
	email: "joe@email.net",
	product: { name: "diapers", quantity: 100 },
	orderDate: new Date(),
	store: "daddy.net"
});

joe.sendConfirmationMail();
joe@email.net: Dear Joe, thank you for ordering 100 diapers on daddy.net

Assertions for custom validation tests

You can add to your models any number of assertions that are custom test functions applied on the values of instances of the model. For example, we have this weird behaviour in JavaScript that makes NaN and Infinity considered as Number, which is probably not what you want. Just add Number.isFinite as an assertion to get rid of them.

Model
var Integer = Model(Number)
	.assert(Number.isFinite)
	.assert(Number.isInteger, "should be an integer");

function isPrime(n) {
	for (var i=2, m=Math.sqrt(n); i <= m ; i++){
		if(n%i === 0) return false;
	}
	return n > 1;
}

var PrimeNumber = Integer.extend().assert(isPrime);
// extend to not add isPrime assertion to the Integer model
Instance
Integer(Math.sqrt(1));
1

Integer(Math.sqrt(2));
TypeError: assertion failed: should be an integer

PrimeNumber(83);
83

PrimeNumber(87);
TypeError: assertion failed: isPrime

Private and constant properties

Some variable naming conventions are commonly used in JavaScript. For example, a leading underscore is used to specify a _private variable which should not be used outside its class methods. Also, constants are often in ALL_CAPS. Model definitions follow these conventions by making _underscored properties not enumerable and CAPITALIZED properties not writable.

Model
var Circle = Model({
	radius: Number, // public
	_diameter: [Number], // private
	UNIT: ["px","cm","mm"], // constant
	_ID: [Number], // private and constant
});
Instance
var c = new Circle({ radius: 120, UNIT: "px", _ID: 1 });
c.radius = 100;
c.UNIT = "cm";
TypeError: cannot redefine constant UNIT
Object.keys(c); // private variables are not enumerated
["radius", "UNIT"]

You can modify or remove these conventions by overriding Model.conventionForPrivate and Model.conventionForConstant functions.


Array models

Array models validate the array elements types. All the validation options for object models are also available for array model elements: type/value checking, optional properties, union types, enumerations, assertions...

Model
var Family = Model({
	father: Father,
	mother: Mother,
	children: Model.Array(Person), // array of Persons
	grandparents: [Model.Array([Mother, Father])]
	            // optional array of Mothers or Fathers
});
Instance
var joefamily = new Family({
	father: joe,
	mother: ann,
	children: [joanna, "dog"]
});
TypeError: expecting Array[1] to be { name: String, female: Boolean }, got String "dog"

The validation is done on initial array elements passed to the model constructor, then on new elements added or modified afterwards.

Note that for browsers which do not support ES6 proxies, if you expand the array by storing an element at an index greater than the current array length (which is commonly considered as a bad practice), the new element and model assertions may not be validated. Use a mutator method like push or splice instead.

var Cards = Model.Array([Number, "J","Q","K"]);
var Hand = Cards.extend("Joker").assert(function(cards){
	return cards.length === 2;
}, "should have two cards");
// Hand is an array of 2 Numbers, J, Q, K or Joker
var myHand = Hand( [7, "Joker"] );
myHand[2] = "K"; // may not trigger validation this way
myHand.push("K"); // use this instead
TypeError: assertion failed: should have two cards

Function models

Function models provide validation on input (arguments) and output (return value). All the validation options for Object models are also available for Function models. The arguments passed to Model.Function are the types of the arguments the function will receive. The defaults method of Function models let you specify default values for missing arguments at call. Finally, the return method is used to specify the type of the function return value.

Model
var Calculator = Model.Function(Number, ["+","-","*","/"], Number)
	            .defaults(0, "+", 1)
	            .return(Number);
Instance
var calc = new Calculator(function(a, operator, b){
	return eval(a + operator + b);
});

calc(3, "+"); // use default value for 3rd argument
4
calc(6, "*", null);
TypeError: expecting arguments[2] to be Number, got null

To declare methods with classical JavaScript OOP programming, functions are declared in the constructor's prototype. You can do the same with instances of function models. Another option is to declare function models in the model definition, then use the defaults method of the object model to set the implementation in the prototype as well. See the 'Default values' section. The difference is that all the properties in the model definition are required for an object to be considered suitable for the model. In the following example, an object must have a function sayMyName to be valid as a Person, while the function greet is not mandatory.

Model
var Person = Model({
	name: String,
	age: Number,
	// function without arguments returning a String
	sayMyName: Model.Function().return(String)
}).defaults({
	sayMyName: function(){
		return "my name is " + this.name;
	}
});

// takes one argument of type Person, returns a String
var greetFnModel = Model.Function(Person).return(String);
Person.prototype.greet = greetFnModel(function(otherguy){
	return "Hello "+ otherguy.name + ", " + this.sayMyName();
});
Instance
var joe = new Person({ name: "Joe", age: 28 });
var ann = new Person({ name: "Ann", age: 23 });

joe.sayMyName();
my name is Joe
joe.greet(ann);
Hello Ann, my name is Joe
joe.greet("dog");
TypeError: expecting arguments[0] to be {
	name: String,
	age: Number,
	sayMyName: Model.Function().return(String)
}, got String "dog"

Custom error collectors

By default, validation errors are collected every time a model instance is created or modified, and thrown as TypeError exceptions with a message describing all the errors found. It it possible to change this behaviour and add your own error collectors. For example, you may want to notify the user that an error occurred, or send the information to your server for error tracking on production.

You can change the error collector globally (Model.prototype.errorCollector), individually by model (myModel.errorCollector), or just once with validate(obj, myErrorCollector). Error collectors are functions called with an array of all the errors collected during the last model creation or modification. Every error is an object with these properties:

var Student = Model({
	name: String,
	course: [ "math","english","history" ],
	grade: Number
}).assert(function(stu){ return stu.grade >= 60 }, "should at least get 60 to validate semester")

Model.prototype.errorCollector = function(errors){
	console.log("Global error collector caught these errors:");
	errors.forEach(function(error){ console.dir(error); });
};

new Student({ name: "Joanna", course: "sleep", grade: 0 });
Global error collector caught these errors:
{
	message: 'expecting course to be "math" or "english" or "history", got String "sleep"'
	path: "course"
	expected: ["math","english","history"]
	received: "sleep"
}
{
	message: "assertion failed: should at least get 60 to validate semester"
}

Student.errorCollector = function(errors){
	console.log("Student model error collector caught these errors:");
	errors.forEach(function(error){ console.dir(error); });
};

new Student({ name: "Joanna", course: "math", grade: 50 });
Student model collector caught these errors:
{
	message: "assertion failed: should at least get 60 to validate semester"
}

Student.validate({ name: "Joanna", course: "cheating", grade: 90 }, function(errors){
	console.log("This specific error collector caught these errors:");
	errors.forEach(function(error){ console.dir(error); });
});
This specific error collector caught these errors:
{
	message: 'expecting course to be "math" or "english" or "history", got String "cheating"'
	path: "course"
	expected: ["math","english","history","science"]
	received: "cheating"
}

Full API

Model Model(definition)
Constructor for basic models. If a litteral object is passed, return an object model.
Model.Object Model.Object(definition)
Constructor for object models
Model.Array Model.Array(itemDefinition)
Constructor for array models
Model.Function Model.Function(definitionArgument1, definitionArgument2, ...)
Constructor for function models
Model.conventionForConstant function(variableName)
Internal function used to identify a constant property based on naming convention. You can override it to suit your needs.
Model.conventionForPrivate function(variableName)
Internal function used to identify a non-enumerable property based on naming convention. You can override it to suit your needs.

Model methods and properties

definition model.definition
Returns the model definition
assertions model.assertions
Returns the list of model assertions
errorCollector model.errorCollector = function(errors){ ... }
Function called when validation errors are detected
extend model.extend(...otherDefinitions)
Returns a new model based on the initial model definition merged with other definitions passed. Assertions are preserved too.
assert model.assert(assertion, [description])
Add a test function to the model that must return true to validate the instance.
validate model.validate(object, [errorCollector])
Manually trigger the validation of an object over the model definition. It is automatically done whenever a property in the model definition is changed.
test model.test(object)
Returns true if the object passed validates the model definition, without necessarily being instantiated through the model constructor (a.k.a. duck-typing).

Object models

defaults objectModel.defaults(defaultValuesObject)
Merge the object passed with the model prototype to set defaults values for some model properties.

Function models

defaults functionModel.defaults(defaultValueArgument1, defaultValueArgument2, ...)
Set a default value for function arguments.
return functionModel.return(returnValueDefinition)
Set the definition of the return value. Each call to the function must return a validated value, otherwise an exception will be raised.

Commonly used models

Here are some models that you may find useful. These are not included in the library, so pick what you need or get them all from here

Click here to show the source code of common models examples

Common questions

What is the impact on performance ?

To get dynamic type validation, Object models have to override properties setters so that each assignment passes through a function that acts as a proxy. This has a cost, especially on old browsers. Therefore, it is not advisable to use object models in performance-critical parts of your applications. In particular, Array models and circular references in models have the most impact on performance. But in general, the loss of time does not exceed a few milliseconds and is quite negligible.

How do I declare a constructor function to be called on instanciation before validating my model ?

The easiest way is to declare this function as a property of your model constructor, then use it to instanciate your models instead of the model constructor itself:

var User = Model.Object({
    firstName: String,
    lastName: String,
    fullName: String
});

User.create = function(properties){
    properties.fullName = properties.firstName + " " + properties.lastName;
    return new User(properties);
};

var joe = User.create({ firstName: "Joe", lastName: "Dalton" });

How can I get the model from an instance ?

If this instance has been properly instantiated with the model constructor, you can retrieve a reference to this model through Object.getPrototypeOf(instance).constructor. This is useful to get the property type of an instance attribute for example.

var User = Model({ name: String }),
	joe = User({ name: "Joe" });

var modelOfJoe = Object.getPrototypeOf(joe).constructor;
// modelOfJoe === User
// modelOfJoe.definition.name === String

How should I deal with circular references in my model definitions ?

You can't refer to a model constructor or model instance that is not yet defined, so you have to update the definition afterwards:

var Honey = Model({
   sweetie: undefined // Sweetie is not yet defined
});

var Sweetie = Model({
   honey: Honey
});

Honey.definition.sweetie = [Sweetie];

var joe = Honey({ sweetie: undefined }); // ann is not yet defined
var ann = Sweetie({ honey: joe });
joe.sweetie = ann;

I use an object like a map with a number of key-value pairs. How do I check the type of all the values ?

This feature is not built-in so that we can make room later for ES6 Maps, which are made for that job. But it is quite easy to get this kind of model using an assertion. Note that any new keys added to the map afterwards will not trigger validation, as they are not part of the model definition. You will have to call YourMapModel.validate(map) manually.

Model.ObjectMap = function(valueType){
	var valueModel = Model(valueType);
	return Model({}).assert(function checkValueTypes(map){
		return Object.keys(map).every(function(key){
			return valueModel.test(map[key]);
		});
	});
}

var REGEX_MMDDYYYY = /^(1[0-2]|0[1-9])/(3[01]|[12][0-9]|0[1-9])/[0-9]{4}$/;
var BirthdayList = Model.ObjectMap([Date, REGEX_MMDDYYYY]);
var friendsBirthday = BirthdayList({
	"Joe": new Date(1985,6,3),
	"Jim": "02/22/1987",
	"Jack": null
});
TypeError: assertion failed: checkValueTypes

I have a question / suggestion / bug to report

Please check the documentation twice, then open an issue on the Github repository