protypes

1.1.0 • Public • Published

PLEASE REFER TO THE FULL DOCUMENTATION ON THE PROTYPES HOMEPAGE!
Information provided in this README has illustrative purpose only, is limited and doesn't describe API of this library in details.

protypes is: Joins Multiple Implementations Into A Single Class.

yarn add protypes
npm i protypes

Table of Contents

What Is Protype?

JavaScript is prototype-based language. Prototypes are objects from which other objects are created, for example, using Object.create(). Since introduction of classes in ES5 standard, the programming was largely class-based, however there's another possibility: via protypes.

A protype is the type of prototypes. It's like a class but simpler, denoted in a standard object. Class-based approach is really a syntactic sugar over prototypes. There are limitations to the standard hierarchical model, such that larger systems can not be adequately expressed in terms of gradually refining implementations of specialising interfaces, and types need to be subtyped via alternative routes, that is, protypes, which simply allow to copy methods into objects without vertical inheritance.

// Standard class-based model:
 
class Doer {
  do() {
    console.log('doing work')
  }
}
class Writer {
  write() {
    console.log('hello world')
  }
}
 
// `MyClass` can inherit from `Doer`, but
// how can we make it extend `Writer`, too?
class MyClass extends Doer {}
 
// Absence of multiple inheritance
// is a limitation of JavaScript
// solved by *Protypes*.

There's no support for multiple inheritance in JS, so the code reuse for types that specialise in multiple things isn't possible without a dynamic solution such that is provided by this library. Protypes allows to subtype types from their supertypes, i.e. to inherit methods horizontally rather than vertically:

import { subtype } from '../..'
 
const Doer = {        // <- protype
  do() {
    console.log('doing work')
  },
}
const Writer = {      // <- protype
  write() {
    console.log('hello world')
  },
}
class MyClass {} // <- target class
 
subtype(MyClass, null, Doer, Writer)
const c = new MyClass()
c.do()
c.write()
doing work
hello world

As you can see, protypes is a more natural approach to prototype-based programming that is facilitated by JavaScript. Only because of the lack of theoretical background in programming and the mainstream approach to development that involves classes, they've been used very rarely. But once you discover their utility, you will fully realise their potential. Plus, nobody is taking classes away, and protypes can be mixed in any class with its hierarchical inheritance structure. When it becomes illogical to work "in depth", then protypes with their "in width" approach will come to the rescue.

API

The package is available by importing its named functions and symbols:

import { subtype, is, $name } from 'protypes'

They are briefly described on this page, but the full description is found in the official documentation.

subtype(Target, opts, ...(Protype|Class))

Subtyping is a feature of OOP which allows objects of one type to be accepted in contexts expecting another type (class) [Sny86]. If you ever used down-casting, you were using this feature. It is also the basis for polymorphism. Just see the example below to immediately understand what subtyping is:

class Doer {
  do() {
    console.log('doing work')
  }
}
class MyClass extends Doer {}
 
/**
 * The main function.
 * @param {Doer} doer 
 */
function main(doer) {
  doer.do()
}
 
const c = new MyClass()
 
main(c)

We defined a class, Doer, and implemented the do method in it. We also declared a function main that accepts a Doer as its argument. We then proceed to create a new instance of a MyClass which extends the Doer class. And although the function main receives this instance, it's still type-sound since the instance is a subtype of Doer (which is a supertype of MyClass). This is what subtyping is.

doing work

When we want to extend classes with protypes (or other classes), we're subtyping the target class by those protypes (or classes). In formal notation, it would be written as Target <: Protype (Subtype <: Supertype). The target can thus become a subtype of any number of other types. Objects created via such class constructor, will inherit all methods from all its supertypes. Inheritance doesn't necessary mean standard top-down sharing of methods via the extends keyword, but can also mean horizontal borrowing of methods from protypes.

SourceOutput
import { subtype } from 'protypes'
 
class Doer {
  do() {
    this._isDoing = true
    console.log('doing work')
  }
  get isDoing() {
    return this._isDoing
  }
}
const Writer = {
  what: 'world',
  write() {
    console.log('hello %s', this.what)
  },
}
class MyClass {} // <- target class
 
subtype(MyClass, null, Doer, Writer)
const c = new MyClass()
c.do()
c.what = 'WORLD'
c.write()
console.log('is doing: %s', c.isDoing)
doing work
hello WORLD
is doing: true

The subtype accepts the target class as the first argument, some options (if no options are required, null needs to be passed), and then the list of all protypes and classes that need to be subtyped. Their methods, getters, setters and even static data fields will be copied across.

There are some nuances that need to be considered, such as using super keyword, which is actually statically bound to the prototype's instance, and merging getters and setters. You can follow the links to read about these intricacies on the package's documentation website.

Options

The list of options that can be used with the subtype method includes:

setProtypesPrototype: Protypes will usually come in form of simple objects. To be able to use super. in their methods, their prototype needs to be set to the target's, but this can be done only once since super is statically bound. Show example of setting prototypes of protypes.
import { subtype } from 'protypes'
 
class Parent {
  method() {
    console.log('parent method')
  }
}
class Target extends Parent {}
let Protype = {
  run() {
    super.method()
  },
}
subtype(Target, null, Protype)
let t = new Target()
try {
  t.run()
} catch (err) {
  console.log(err.message)
}
// fix super
class Target2 extends Parent {}
subtype(Target2, {
  setProtypesPrototype: true,
}, Protype)
= new Target2()
t.run()
(intermediate value).method is not a function
parent method
[paid] methodDecorators: An array of decorators that can wrap methods which are being assigned to the prototype of the target class. Show example of method decorators.
import { subtype } from 'protypes'
 
class Target {}
let Protype = {
  run(arg) {
    console.log('running protype with %s', arg)
  },
}
const decorator = (method, propName) => {
  return function(...args) {
    console.log('pre-condition on %s', propName)
    const res = method.call(this, ...args)
    console.log('post-condition on %s', propName)
    return res
  }
}
subtype(Target, {
  methodDecorators: [decorator],
}, Protype)
const t = new Target()
t.run('hello')
pre-condition on run
running protype with hello
post-condition on run
[paid] mergeGettersSetters: When a class or a protype specifies only a getter or setter without its counter-part, the inheritance will be broken. This option allows to look up missing getters/setters in the prototype chain of the target. Show example.
Without MergingWith Merging
import { subtype } from 'protypes'
 
class Target {
  set example(e) {
    this._e = e
  }
}
 
let Protype = {
  get example() {
    return this._e
  },
}
 
subtype(Target, null, Protype)
let t = new Target()
t.example = 'example'
 
 
 
// expect undefined
console.log(t.example)
undefined
import { subtype } from 'protypes'
 
class Target2 {
  set example(e) {
    this._e = e
  }
}
 
let Protype = {
  get example() {
    return this._e
  },
}
 
subtype(Target2, {
  mergeGettersSetters: true,
}, Protype)
let t2 = new Target2()
t2.example = 'example'
 
// expect "example"
console.log(t2.example)
example
[paid] fixTargetGettersSetters: Similar to the previous option, but applies to the target itself. Can be applied before or after assignment of protypes/classes. Click to preview example code.
Without FixWith Fix
import { subtype } from 'protypes'
 
class Parent {
  get example() {
    return this._e
  }
}
class Target extends Parent{
  set example(e) {
    this._e = e
  }
}
 
subtype(Target, null)
let t = new Target()
t.example = 'example'
console.log(t.example)
undefined
import { subtype } from 'protypes'
 
class Parent {
  get example() {
    return this._e
  }
}
class Target extends Parent{
  set example(e) {
    this._e = e
  }
}
 
subtype(Target, {
  fixTargetGettersSetters: true,
})
let t = new Target()
t.example = 'example'
console.log(t.example)
example
[paid] bindings: Allows to set bindings on method's class automatically, so that they can be destructured and still access the this keyword inside of them.
import { subtype } from 'protypes'
 
class Target {
  get target() { return 'target' }
  test() { console.log('test:', this.target) }
}
 
subtype(Target, {
  bindings: {
    bindMethods: true,
  },
}, {
  get protype() { return 'protype' },
  example() { console.log('example: %s, %s',
    this.protype, this.target) },
})
 
let t = new Target()
const { example, test } = t
test()
example()
test: target
example: protype, target

is(obj, protypeName)

To determine whether the given instance's class is Proto|Class or a subclass of Proto|Class. In plain JS, we'd use instanceof however with multiple inheritance / subtyping this does not work as the methods are copied across into the target class. The is method from this library can be used to determine whether a Protype (or Class) has been implemented by the given object.

SourceOutput
import {
  subtype, $name, is,
} from 'protypes'
 
class Doer {
  get [$name]() { return 'Doer' }
}
const Writer = {
  [$name]: 'Writer',
}
class MyClass {} // <- target class
 
subtype(MyClass, null, Doer, Writer)
const c = new MyClass()
console.log('is Writer:',
  is(c, 'Writer'))
console.log('is Doer:',
  is(c, 'Doer'))
console.log('is Member:',
  is(c, 'Member'))
is Writer: true
is Doer: true
is Member: false

The check is performed on strings currently, but in the future version it'll be extended to passing of actual protypes. The reason for this limitation is that with non-string based implementation of the is method, chances are high you might run into circular dependencies that would break the system. The name must be defined on protypes and classes using the special $name symbol exported by the library.

Copyright & License

The public free version of the package is provided under the GNU Affero General Public License v3.0 so that you can use it in AGPL-compatible projects. The full version requires purchasing of a license key and is distributed under an EULA terms. Please contact Art Deco on Keybase chat to purchase the key.

Art Deco © Art Deco™ 2020

Package Sidebar

Install

npm i protypes

Weekly Downloads

1

Version

1.1.0

License

AGPL-3.0

Unpacked Size

62.3 kB

Total Files

8

Last publish

Collaborators

  • zvr