Object Model

Strong Dynamically Typed Object Modeling for JavaScript

What is this library ?

A recurring criticism of JavaScript is that it is a weakly typed language. New languages that compile to JavaScript have been invented, such as TypeScript by Microsoft. We also have static analysis tools like Flow by Facebook. These solutions bring static typing, which means 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 intended types.

Static typing is insufficient to prevent most of the real and practical bugs caused by type errors. Indeed, JavaScript applications often involve 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 own code that is under their control and can benefit from type inference with their IDE.

This is why Object Model is about strong dynamic type checking: it aims to get strong validation constraints for your variables at runtime. Whenever a property is modified, the whole object is validated against its 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 plain old JS 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:

  • Typed object structures
  • Typed arrays
  • Typed functions
  • Union types
  • Enumerations
  • Custom assertions
  • Optional properties
  • Default values
  • Null-safe object traversal
  • Easy composition and inheritance through model extensions
  • Constants and private properties based on name conventions
  • Explicit error messages
  • Custom error collectors
  • all in 8 kB minified, 3.4 kB gzipped

Download

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

Latest 2.x version: v2.6.4

Changelog and previous releases

Checkout the Github Releases

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. Basic models validate the argument passed against the model definition, and return the validated value.

Model
var NumberModel = Model(Number);
// 'new' keyword is optional for models and model instances
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

Usage with ES6 classes

If you can use ES6 on your project, it is very easy to define a model for your classes:

Model
class Character extends Model({ lastName: String, firstName: String }){
   get fullName(){ return `${this.firstName} ${this.lastName}`; }
}
Instance
var rick = new Character({ lastName: "Sanchez", firstName: "Rick" });
rick.lastName = 132;
TypeError: expecting lastName to be String, got Number 132
console.log(rick.fullName); // "Rick Sanchez"

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({
	// can be a Number or a String
	delay: [Number, String],

	// optional property which can be a Boolean or a String
	easing: [Boolean, String, undefined]
});
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 and instanciation
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 assignment

You can set a default value for any model with model.defaultTo(value). This default value will be used if the argument passed to the model constructor is undefined.

Model
var N = Model(Number).defaultTo(1)
Instance
N(5) + N() === 6

To specify default values for some properties of your object models, 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 models. Note that the property values do not necessarily need to be model instances to be considered valid: only the definition of the associated 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".

When a model definition is recognized, the value is automatically replaced by an instance of the corresponding model. This naive approach is very time saving and allows you, for example, to parse composed models from JSON in one step. If there is somehow an ambiguity (such as two possible valid models within an union type), the value is kept unchanged and a warning console message will inform you how to solve this ambiguity.

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

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

var couple = Lovers({
   husband: joe,  // object duck typed
   wife: ann // object model
});

couple.husband instanceof Person === true // object has been casted to Person

Inheritance by extension

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

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

Note that the extend function can receive any number of additional models and/or definitions as arguments. They will be merged using this strategy:


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 model instances. All assertions are called every time the model is changed, and must all return true to validate. Exceptions thrown by assertions are catched and considered as assertion failures.

For example, we can get an Integer model by adding Number.isInteger as an assertion to a basic Number model.

Model
var PositiveInteger = Model(Number)
	.assert(Number.isInteger)
	.assert(function(n){ return n >= 0 }, "should be greater or equal to zero")

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 = PositiveInteger.extend().assert(isPrime);
// extend to not add isPrime assertion to the Integer model
Instance
PositiveInteger(-1);
TypeError: assertion should be greater or equal to zero returned false for value -1

PositiveInteger(Math.sqrt(2));
TypeError: assertion isInteger returned false for value 1.4142135623730951

PrimeNumber(83);
83

PrimeNumber(87);
TypeError: assertion isPrime returned false for value 87

Assertions are inherited from Model.prototype so you can add global assertions on all models. The second argument of the assert method is an optional message shown when assertion fails. It can be a String or a function returning a String.

Configuration
Model.Object.prototype.assert(function noUndeclaredProperties(obj) {
	var modelDefinition = this.definition;
	var extraKeys = Object.keys(obj).filter(function(key){
		return modelDefinition.hasOwnProperty(key) === false;
	});
	if(extraKeys.length === 0) return true; // validate assertion
	else return extraKeys; // pass information to the error message
}, function(extraKeys){
	return "These properties are not defined in the model: " + extraKeys.join(', ');
})
Example
var Order = Model({ price: Number });

var expectedOrder = Order({ price: 42.50 });

var receivedOrder = Order({
	price: 42.50,
	taxes: 8.33,
	deliveryCost: 15.00
});
TypeError: These properties are not defined in the model: taxes, deliveryCost

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.

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

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"]

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, 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 "should have two cards" returned false for value [7, "Joker", "K"]

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. 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

Under classical JavaScript OOP programming, methods 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 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
Person.prototype.greet = Model.Function(Person).return(String)(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: Function() => 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.

Error collectors are callback functions called with an array of all the errors collected during the last model inspection. Every error is an object with these properties:

  • message: a message describing the error
  • [expected]: the expected type definition or assertion
  • [received]: the received value, to compare to the expected
  • [path]: the path where the error occurred in an object model definition

Global error collector

This is how you define an error collector globally for all models.

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

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")

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 should at least get 60 to validate semester returned false for value { name: "Joanna", course: "sleep", grade: 0 }",
	path: null,
	expected: function(stu){ return stu.grade >= 60 },
	received: { name: "Joanna", course: "sleep", grade: 0 }
}

Model error collector

This is how you define an error collector specifically by model

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 should at least get 60 to validate semester returned false for value { name: "Joanna", course: "math", grade: 50 }",
	path: null,
	expected: function(stu){ return stu.grade >= 60 },
	received: { name: "Joanna", course: "math", grade: 50 }
}

Single-use error collector

And this is how you define an error collector to be used only once with validate(obj, myErrorCollector)

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
default model.default
Returns the default value if defined
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.
defaultTo model.defaultTo(defaultValue)
Shorthand to set the default value of the model
test model.test(value)
Returns true if the value passed validates the model definition. Works with duck typing.
validate model.validate(instance, [errorCollector])
Manually triggers the validation of a model instance over its definition. It is automatically done whenever an instance property changes.
Note that contrary to the test method, the object is not cast to its suitable model (a.k.a duck typing) before being validated.

Object models

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

Function models

defaults functionModel.defaults(defaultValueArgument1, defaultValueArgument2, ...)
Set default values for the 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

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.2.

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 recommended way is to use a factory function to instanciate your models. You can declare as many different factories as needed, which makes this pattern both simple and flexible.

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 ?

With the constructor property. If this property is used in your model, you can also retrieve it with Object.getPrototypeOf(instance).constructor. This is useful for retrieving the type of a property for example.

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

var modelOfJoe = joe.constructor // or 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 or 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.

Implementation
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]);
		});
	});
}
Example
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 checkValueTypes returned false for value {
	Joe: Wed Jul 03 1985 00:00:00,
	Jim: "02/22/1987",
	Jack: null
}
How can I serialize/deserialize objects while preserving type information ?

Serializing in JSON necessarily implies that you lose the type information, except if you store it manually with your data, then retrieve it with a custom parsing function. It is for the best to let you decide how you want to store the type information within your data.

Here is a proposal of implementation using a simple { _value, _type } wrapper:

Implementation
Model.prototype.serialize = function(instance){
	var availableModels = Object.keys(Model.serializables);
	return JSON.stringify(instance, function(key, value){
		var modelName = availableModels.find(function(name){
			return value instanceof Model.serializables[name];
		});
		if(modelName && key !== "_value"){
			return { _type: modelName, _value: value }
		}
		return value;
	}, '\t');
}

Model.prototype.parse = function(json){
	return JSON.parse(json, function(key, o){
		if(o && o._type in Model.serializables){
			return new Model.serializables[o._type](o._value);
		}
		return o;
	})
}
Example
var Type1 = Model({ content: String }).defaultTo({ content: 'Content 1' }),
    Type2 = Model({ content: String }).defaultTo({ content: 'Content 2' }),
	Container = Model({ items: Model.Array(Type1, Type2) });

// List all your serializable models here
Model.serializables = { Container, Type1, Type2 };

var a = new Container({ items: [new Type1, new Type2] });

var json = Container.serialize(a);
console.log(json);

var b = Container.parse(json);
console.log(
	b instanceof Container,
	b.items[0] instanceof Type1,
	b.items[1] instanceof Type2
);

I have a question / suggestion / bug to report

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

Like what you see ? Share it with the world !