value-object

value-object.js - simple value objects

Downloads in past

Stats

StarsIssuesVersionUpdatedCreatedSize
value-object
1110.4.35 years ago7 years agoMinified + gzip package size for value-object in KB

Readme

value-object.js
Value Object - objects that matter only as the combination of their properties. Two value objects with the same values for all their properties are considered equal.
This library provides a convenient way to define strict, immutable value objects.

Install

npm install value-object

Defining value objects

Use subclasses to define value objects with type constraints:
const ValueObject = require('value-object')

class Currency extends ValueObject.define({
  code: 'string',
  name: 'string'
}) {}

class Money extends ValueObject.define({
  currency: Currency,
  amount: 'number'
}) {}

...or don't use classes, if you prefer:
const Money = ValueObject.define({
  amount: 'number',
  currency: { code: 'string' }
})

Instantiating value objects

Use the new keyword, passing values for each property:
const gbp = new Currency({ code: 'GBP', name: 'British Pounds' })
const price = new Money({ currency: gbp, amount: 12.34 })
const other = new Money({ currency: { code: 'USD', name: 'US Dollars' }, amount: 14.56 })

Constraints prevent value objects from being instantiated with invalid property values.

Unexpected types

Property values with unexpected types are rejected:
> new Currency({ code: 'USD', name: 123 })

Error: Currency was constructed with invalid property values
  Expected: { code:string, name:string }
  Actual:   { code:string, name:number }
    name is invalid:
      Expected string, was number

Unrecognised properties

Value objects cannot be instantiated with unrecognised properties:
> new Currency({ code: 'NZD', name: 'New Zealand Dollars', colour: 'All black' })

Error: Currency was constructed with invalid property values
  Expected: { code:string, name:string }
  Actual:   { code:string, name:string, colour:string }
    colour is invalid:
      Property is unexpected

Missing properties

Value objects cannot be instantiated with missing properties (unless they are optional):
> new Money({ amount: 123 })

Error: Money was constructed with invalid property values
  Expected: { currency:Currency, amount:number }
  Actual:   { amount:number }
    currency is invalid:
      Property is missing

Setting properties to null

Properties can be set to null:
> new Money({ currency: null, amount: null })

Money { currency: null, amount: null }

Setting properties to undefined

Properties cannot be set to undefined (unless they are optional):
> new Money({ currency: null, amount: undefined })

Error: Money was constructed with invalid property values
  Expected: { currency:Currency, amount:number }
  Actual:   { currency:null, amount:undefined }
    amount is invalid:
      Expected number, was undefined

Built-in property types

Properties can be declared with built-in type constraints:
class Manager extends ValueObject.define({
  firstName: 'string',
  age: 'number',
  trained: 'boolean',
  subordinates: 'object',
  preferences: 'any'
}) {}

  • string: expects a value where typeof value === 'string'
  • number: expects a value where typeof value === 'number'
  • boolean: expects a value where typeof value === 'boolean'
  • object: expects a value where typeof value === 'object'
  • any: expects any non-null value

Optional properties

Properties declared with ? can be set to null or undefined, or omitted altogether:
class Options extends ValueObject.define({
  age: 'number?',
  aliases: 'object?',
  colour: 'string?',
  checked: 'boolean?'
}) {}

new Options({ age: null, aliases: {}, colour: undefined })
// => Options { age: null, aliases: {}, colour: undefined }

Optional properties can also be declared with ValueObject.optional():
class IceCream extends ValueObject.define({
  flavours: ValueObject.optional(['string'])
}) {}

new IceCream({ flavours: ['mint', 'chocolate'] })
// => IceCream { flavours: [ 'mint', 'chocolate' ] }

new IceCream({})
// => IceCream {}

Array properties

Arrays with arbitrary elements can be declared with the type Array:
class Person extends ValueObject.define({
  favouriteThings: Array
}) {}

new Person({ favouriteThings: ['cheese', 69, null] })

Generic array properties

Arrays with value constraints are declared by wrapping the type definition (e.g. 'number', Date) in []:
class Point extends ValueObject.define({
  x: 'number',
  y: 'number'
}) {}

class Polygon extends ValueObject.define({
  vertices: [Point] // instances of Point
}) {}

new Polygon({
  vertices: [
    new Point({ x: 1, y: 2 },
    new Point({ x: 3, y: 4 }
  )]
})

User-defined properties

Custom property types can be defined with ValueObject.definePropertyType() and then used later by name in ValueObject.define():
ValueObject.definePropertyType('money', () => ({
  coerce(value) {
    if (typeof value === 'string') {
      const parts = value.split(' ')
      return { value: { amount: Number(parts[0]), currency: parts[1] } }
    }
    return { failure: 'Only string values allowed' }
  },

  areEqual(a, b) {
    return a.currency == b.currency && a.amount == b.amount
  },

  describe() {
    return '<money>'
  }
}))
class Allowance extends ValueObject.define({ cash: 'money' }) {}

Property constraints are expressed as a function that returns a value with the following methods:
  • .coerce(value) converts an arbitrary value to the final property value.
Expected to return { value } when converting the property value is successful or { failure } with a message when converting fails.
  • .areEqual(a, b) returns true if two instances of the type are considered equal, or false otherwise.
  • .describe() returns a string used in error messages mentioning the property.

The constraint is used to convert property values from other types according to its .coerce(value) method:
> new Allowance({ cash: '123.00 GBP' })

Allowance { cash: { amount: 123, currency: 'GBP' } }

...and its .describe() method is used in error messages:
> new Allowance({ cash: 666 })

Error: Allowance was constructed with invalid property values
   Expected: { cash:<money> }
   Actual:   { cash:number }
   cash is invalid:
     Only string values allowed

Equality

Value objects are considered to be equal if their properties are equal. Equality of two objects is tested by calling valueObject.isEqualTo(otherValueObject):
gbp.isEqualTo(new Currency({ code: 'GBP', name: 'British Pounds' }))
// => true

gbp.isEqualTo(new Currency({ code: 'EUR', name: 'Euros' }))
// => false

const gbpPrice = new Money({ amount: 123, currency: gbp })
const eurPrice = new Money({ amount: 123, currency: eur })
gbpPrice.isEqualTo(eurPrice)
// => false

eurPrice.isEqualTo(new Money({ amount: 123, currency: eur }))
// => true

Reflection

ValueObject types have a schema property that allows reflection over properties and arbitrary metadata associated with each property:
class Product extends ValueObject.define({
  name: 'string',
  stockLevel: ValueObject.property('number', {
    default: 0,
    description: 'units in stock'
  })
}) {}

> Product.schema.properties.stockLevel

Property {
  constraint: Primitive { cast: [Function: Number], name: 'number' },
  metadata: { default: 0, description: 'units in stock' },
  optional: false }

Creating new value objects from existing value objects

Use with(newAttributes) to create new value objects, with new values for a specific set of properties:
const salePrice = price.with({ amount: 12.0 })
salePrice.currency.code
// => 'GBP'

Converting value objects to plain objects

Use toPlainObject() to create a plain old mutable object from a value object's property values:
> JSON.stringify(gbp.toPlainObject())

{ "code": "GBP", "name": "British Pounds" }

Any value-object instances will be converted using their schemas. Any objects that are not value-object instances will be cloned using JSON.parse(JSON.stringify(object)) by default. Pass in an optional clone function to override this behaviour:
valueObject.toPlainObject(fancyDeepCloneFunction)

Converting value objects to JSON

Use toJSON() to create an object with __type__ properties for subsequent deserialization:
> JSON.stringify(gbp.toJSON())

{ "__type__": "Currency", "code": "GBP", "name": "British Pounds" }

Converting value objects from JSON

Use ValueObject.deserializeForNamespaces() to create a deserialize function that can turn the resulting JSON string back into objects
const deserialize = ValueObject.deserializeForNamespaces([{ Currency }])
const gbp2 = deserialize('{"__type__":"Currency","code":"GBP","name":"British Pounds"}')
gbp2.isEqualTo(gbp)
// => true

Immutability

Value objects cannot be updated. Use strict mode to throw errors when attempts to set property values are made.
gbp.code = 'USD'
// TypeError:Cannot assign to read only property 'amount' of object '#<Currency>