value-object

0.4.3 • Public • Published

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>

Dependencies (0)

    Dev Dependencies (21)

    Package Sidebar

    Install

    npm i value-object

    Weekly Downloads

    44

    Version

    0.4.3

    License

    MIT

    Unpacked Size

    35.6 kB

    Total Files

    4

    Last publish

    Collaborators

    • joshski
    • tooky