A Proper Interface
A more proper interface system for javascript
Note: This is currently a work in progress! There may be (possibly breaking) bugs, and updates currently do not have a schedule. If you use this module and encounter a bug, please post it on the issues page. This package will be updated many times before it is mature, and updates may not be backwards compatable. Please check for updates frequently!
Installation
npm -i a-proper-interface
or
yarn add a-proper-interface
Usage
Usage was made to be simple (as can be, given the circumstances)
Somewhere near the entrance to your code, put require('a-proper-interface')
and you're good to go
Note:
the implements
property is a global symbol, and needs to be accessed using brackets [implements]
. The [implements]
property exists on all classes extended from Implementable()
, and behaves differently depending on whether it was called statically or not, but always expects an interface symbol or class (or an array of such)
When called statically, it is declaring that this class implements the interface(s) provided, and constructs an instance enforced to that/those interface(s), taking optionally the second argument to [implements]
as the arguments to the constructor (if the second argument is an array, it is spread into the constructor). It returns the instance created, which is enforced by the interface(s)
When called on an instance, it either: (a) returns the implemented interfaces if no arguments are provided, or (b) if an argument is provided, returns a boolean indicating if the instance has been enforced to the interface(s if the argument is an array)
Declaring/Using an interface
Option 1 - Symbols
Since symbols cant be extended, using the symbols generated by this system effectively eliminates any worry of the interface being extended directly (which bypasses the enforcement of the interface). This method is faster, but has a larger footprint due to the presence of a lookup table of compiled interfaces stored internally.
//someInterface.js
class SomeInterfaceClass {}
module.exports = interface(SomeInterfaceClass)
//someImplementation.js
class SomeImplementation extends Implementable() {}
SomeImplementation[implements](require('someInterface.js'))
Option 2 - No Symbols
This method exists as an opposite to the first, bypassing symbol generation and pre-compilation of the interface class. This method has a smaller footprint, but performs slower due to having to compile the interface before enforcing it.
//someInterface.js
class SomeInterfaceClass {}
module.exports = SomeInterfaceClass
//someImplementation.js
const SomeInterface = require('./someInterface.js')
class SomeImplementation extends Implementable() {}
SomeImplementation[implements](SomeInterface)
and that's it
Everything from the number of arguments expected on functions (even the constructor) and the type of the member down to whether the member is static or not is compared and enforced
Using multiple interfaces
You can also provide an array of interfaces to implement, even mixing symbols and direct classes
//someInterface1.js
class SomeInterfaceClass1 {}
module.exports = SomeInterfaceClass1
//someInterface2.js
class SomeInterfaceClass2 {}
module.exports = SomeInterfaceClass2
//someImplementation.js
const SomeInterface1 = require('./someInterface1.js')
const SomeInterface2 = require('./someInterface2.js')
class SomeImplementation extends Implementable() {}
SomeImplementation[implements]([SomeInterface1,SomeInterface2])
Retaining the ability to extend
It might not look like it since the Implementable
funtion is in the way, but extending classes is still possible by passing the class into the Implementable
function
//someInterface.js
class SomeInterfaceClass {}
module.exports = interface(SomeInterfaceClass)
//someBaseClass.js
class SomeBaseClass {}
module.exports = SomeBaseClass
//someImplementation.js
const SomeInterface = require('./someInterface.js')
const BaseClass = require('./someBaseClass.js')
class SomeImplementation extends Implementable(BaseClass) {} // SomeImplementation now extends BaseClass
SomeImplementation[implements](SomeInterface)
This works because, interally, the Implementable
function returns an Implementable
class, and if a class is passed to the Implementable
function, it extends the Implementable
class from it. Otherwise, the Implementable
class is extended from an empty class named EmptyBaseImplementation
.
Checking if an instance implements an iterface
//someInterface.js
class SomeInterfaceClass {}
module.exports = interface(SomeInterfaceClass)
//someImplementation.js
const SomeInterface = require('./someInterface.js')
class SomeImplementation extends Implementable() {}
const inst = SomeImplementation[implements](SomeInterface,[{test:"arguments"}])
inst[implements](SomeInterface) // true, or false 'implements' was never called statically with the interface
Todo
- [x] Add filter to selectively enforce parts of an interface
- [x] Add basic example
- [x] Add system operation details in README.md Added to Usage section
- [ ] Add in-depth examples
- [x] Add usage in README.md
- [x] Add demonstration of multiple interfaces in README.md
- [x] Add demonstration of checking if class implements interface
- [x] Add demonstration of still being able to extend classes
- [x] Add custom errors Added as file Errors. Exported object contains all error classes
- [x] Move functionality to separate classes to declutter index file All functionality moved to files in util
- [x] Add license License is MIT. To add: Anyone and everyone can use/modify at will. All I ask is that the core team be reminded that interfaces need to be in javascript
- [x] Enforce instance and static members separately implemented in filter feature
- [ ] Add sub-interfaces
- [x] Add better detection of global object
- [x] Add non-global version no-globals Non-global version exports all functionality instead of attaching to global
- [ ] Bug hunting
- [ ] Add usage example of non-global version
- [ ] Add utilities to aid in enforcing returns from functions/constructors that expect input