@etchedjs/etched

A utility to easily create some immutable objects, based on multiple etched inheritance.

Showing:

Popularity

Downloads/wk

305

GitHub Stars

7

Maintenance

Last Commit

4mos ago

Contributors

1

Package

Dependencies

0

License

MIT

Type Definitions

Tree-Shakeable

No?

Categories

Readme

@etchedjs/etched

Etches your JS objects in stone

A utility to easily create some immutable objects, based on multiple etched inheritance.

It provides:

  • Fully immutable
  • Multiple etched inheritance
  • Lazy-setters, only declare the wanted properties and optionally validate them
  • Inherited constants, declare the properties that can't be overridden on the instances
  • Reduced prototype chain, an etched object always have its model as prototype
  • Auto-reconcile, based on the model, the etched objects never take any unwanted properties

How etched resolves the (multiple) inheritance problems

The diamond problem

✓ The etched objects are immutables, then there is only one possible implementation for an instance... and the members are merged on the instance itself (no super()).

The member collisions

✓ There is no way to redeclare a member with the same name, except if it's strictly the same member, it doesn't rely on the value.

Mocking difficulties

✓ By design, the inheritance is based on composition, you can easily mock everything as you want.

Install

npm i @etchedjs/etched

Alternatively, in a browser, you can use it from the CDN:

import * as etched from 'https://unpkg.com/@etchedjs/etched@latest/etched.js'

A concrete example

import { etch, etches, model } from '@etchedjs/etched'

const entity = model({
  set id (value) {
    if (!Number.isSafeInteger(value) || value < 1) {
      throw new ReferenceError('Must be a positive safe integer')
    }
  },
  set createdAt (value) {
    if (!Object.is(Date.prototype, Object.getPrototypeOf(value || {}))) {
      throw new ReferenceError('Must be a Date')
    }
  }
})

const account = model({
  set name (value) {
    if (typeof value !== 'string' || !value.length) {
      throw new TypeError('Must be a non-empty string')
    }
  },
  set score (value) {
    if (!Number.isSafeInteger(value) || value < 0 || value > 10) {
      throw new TypeError('Must be a valid score')
    }
  }
})

const accountEntity = model(entity, account)

const jack = etch(accountEntity, {
  id: 123,
  createdAt: new Date(),
  name: 'Jack',
  score: 9
})

const renamed = etch(jack, {
  name: 'Jack-Renamed',
  score: 10
})

console.log(jack) // {  id: 123, createdAt: 2020-11-12T19:54:12.979Z, name: 'Jack', score: 9 }
console.log(renamed) // {  id: 123, createdAt: 2020-11-12T19:54:12.979Z, name: 'Jack-Renamed', score: 10 }
console.log(etches(entity, accountEntity)) // true
console.log(etches(account, accountEntity)) // true
console.log(etches(entity, jack)) // true
console.log(etches(account, jack)) // true
console.log(etches(accountEntity, jack)) // true
console.log(etches(entity, renamed)) // true
console.log(etches(account, renamed)) // true
console.log(etches(accountEntity, renamed)) // true

API

etched.etched

The default instance

etched.etched
// {}

etched.model

etched.model(...models)

Creates a new immutable model, based on optional models.

It declares constants (direct value) and setters (to validate dynamic values)

It also acts as an instance.

Example

const model = etched.model({
  constant: 123,
  set dynamic (value) {
    if (isNaN(value)) {
      throw new TypeError('Must be a number')
    }
  }
})

Produces:

console.log(type)
// { constant: 123 }

console.log(Object.getPrototypeOf(type))
// { constant: 123, dynamic: Setter }
const extended = etched.model(type, {
  set value(value) {
  }
})

Produces:

console.log(type)
// { constant: 123 }

console.log(Object.getPrototypeOf(type))
// { constant: 123, dynamic: Setter, value: Setter }

namespace({ url }, ...models)

Acts like model(), but flagging the model with a Symbol keyed property with the url as value, useful to easily identify your models.

Example:

const model = etched.namespace(import.meta, {
  constant: 123
})
/*
{
  constant: 123,
  [Symbol(@etchedjs/etched:ns)]: 'file:///DRIVE:/module/location.js'
}
*/

etch.etch(instance, ...mixins)

Creates a new immutable instance, based on a previous one and the optional mixins.

It only takes the values of corresponding properties to a model setter.

Example

const instance = etched.etch(type, {
  dynamic: 456
})
// { constant: 123, dynamic: 456 }

const copy = etched.etch(type, instance, {
  dynamic: 789
})
// { constant: 123, dynamic: 789 }

`etched.etches(model, instance)

Provides a way to check if an instance is an extension of the provided model.

Example

etched.etches(etched.etched, instance)
// true

etched.etches(type, instance)
// true

etched.etches(type, type)
// true

etched.etches(type, {})
// false

etched.fulfill(instance, ...mixins)

Acts as etched.etch(instance, ...mixins) but sets all the instance properties.

Example

const fullfilled = etched.fulfill(type, {
  dynamic: 789
})
// { constant: 123, dynamic: 789 }

etched.fulfill(type, {})
// Throws AggregateError: Unsafe etching
// with errors ['dynamic', TypeError: Must be a number]

`etched.fulfills(model, value)

Provides a way to check if an instance is a fulfilling extension of the provided model.

Example

etched.fulfills(etched.etched, instance)
// true

etched.fulfills(type, instance)
// true

etched.fulfills(type, type)
// true

etched.fulfills(type, {})
// false

etched.iterable

A model to extend to make your etched instances iterables

import { etch, iterable, model } from '@etchedjs/etched'

const picker = model(iterable, {
  set 1 (value) {},
  set 3 (value) {}
})

console.log([...etch(picker, [0, 1, 2, 3, 4])]) // [['1', 1], ['3', 3]]

Additional notes

Cumulative setters

The model setters are cumulative by extension.

const cumulative = etched.model(type, {
  set dynamic(value) {
    if (!Number.isSafeInteger(value)) {
      throw new TypeError('Must be a safe integer')
    }
  }
})

etched.etch(type, {
  dynamic: NaN
})
// Throws AggregateError: Unsafe etching
// with errors ['dynamic', TypeError: Must be a number]

etched.etch(cumulative, {
  dynamic: 0.1
})
// Throws AggregateError: Unsafe etching
// with errors ['dynamic', TypeError: Must be a safe integer]

etched.etch(cumulative, {
  dynamic: 456
})
// { constant: 123, dynamic: 456 }

Unsafe etching

A model etching can't redeclare a constant...

etched.model(type, {
  constant: 456
})
// Throws AggregateError: Unsafe etching
// with errors ['constant', ReferenceError: 'Duplicate constant `constant`']

etched.model(type, {
  set constant(value) {
  }
})
// Throws AggregateError: Unsafe etching
// with errors ['constant', ReferenceError: 'Duplicate constant `constant`']

... but an extension can declare a model property as a constant

const model = etched.model({
  set constant (value) {}
})

const extended = etched.model(model, {
  constant: 456
})
// { constant: 456 }

Licence

MIT

Rate & Review

Great Documentation0
Easy to Use0
Performant0
Highly Customizable0
Bleeding Edge0
Responsive Maintainers0
Poor Documentation0
Hard to Use0
Slow0
Buggy0
Abandoned0
Unwelcoming Community0
100