autocreate
Sometimes, you need something to be both a class and a function. Sometimes, you need a class to be callable without using new
. The autocreate
module makes both as easy as typing @auto
before your class declaration (in Babel/ES7 or TypeScript), and almost that easy in CoffeeScript or plain Javascript.
auto = require('autocreate');
@auto class MyClass {
constructor() {
}
}
autocreate
also includes a special hook method that you can define to customize what happens when the class is called without new
. That way, you can return an object from a cache, return an object that was passed in (like Object(obj)
does), or generaly do things differently when invoked without new
:
@auto class dualUse {
constructor() {
}
__class_call__() {
}
}
For more usage details, including code samples for all of the supported languages, see the "Usage" section, below. (autocreate
will also probably work with most other compile-to-Javascript languages; read the "Implementation Details" section for more info!)
autocreate
supports even the most exotic class features of its supported languages, including static methods and properties, automatically bound methods, etc. -- even non-enumerable property descriptors and getter/setters.
It does not, however, depend on any particular version of Javascript as its execution environment: if you're using a language whose features work on old versions of IE, then autocreate
will work there too! If you're targeting an ES5 or ES6 environment, no problem: autocreate
will detect the relevant features (descriptors, symbols, __proto__
, etc.) and progressively enhance itself to support them.
Contents
Usage
Create a new
-less class
In TypeScript (or Babel w/ES7 features enabled)
auto = require('autocreate');
@auto class MyClass {
constructor() {
}
}
In CoffeeScript
auto = require 'autocreate'
class MyClass
# Because CoffeeScript doesn't have decorators, you have to
# assign to the class name *inside* the class body
MyClass = auto @
constructor: ->
# no need to check `this instanceof`;
# just do whatever needs doing
In Plain Javascript
var auto = require('autocreate');
MyClass = auto(MyClass);
function MyClass () {
});
Create a class with separate "function" behavior
In TypeScript (or Babel w/ES7 features enabled)
@auto class MyClass {
constructor() {
}
__class_call__() {
}
}
In CoffeeScript
class MyClass
MyClass = auto @
constructor: ->
# This will run if `new MyClass()` is used
__class_call__: ->
# This will run if `MyClass()` is called:
# `this` will be `MyClass.prototype`
In Plain Javascript
MyClass = auto(MyClass);
function MyClass () {
});
MyClass.prototype.__class_call__ = function () {
}
Implementation Details
Constructor Wrapping
autocreate
works by creating a new constructor function to wrap the old one. It copies any static methods, properties, descriptors, __proto__
etc., so that even Babel's most advanced class features will still work on the result. It even shares the original constructor's .prototype
and resets the .constructor
, so that created objects will still be an instanceof
both the wrapper and the wrapped constructor.
In-Namespace Replacement
For this to work correctly in a given language, the wrapper must replace the wrapped constructor in the same namespace where it was defined.
For plain Javascript, this is simple: just set MyClass = auto(MyClass);
, and you're done. For other languages, it's a little more complex, because most compile-to-Javascript languages wrap the definition of a class constructor inside a closure.
For TypeScript and Babel (w/ES7 decorators enabled), the @auto
decorator syntax handles this: it actually replaces the constructor inside the closure, in a way that would not always work if you just used MyClass = auto(MyClass);
. (Because generated methods inside the class-closure could refer to the wrong constructor.)
CoffeeScript, however, doesn't have a decorator syntax, which is why you have to do MyClass = auto @
(or MyClass = auto this
) inside the class body. If you do it from outside the class body, all the code inside the class body will refer to the original constructor!
Also, it's important to note that the MyClass
part of the statement must match CoffeeScript's generated class name. That is:
class MyClass
MyClass = auto @ # default
class foo.bar.Baz
Baz = auto @ # last .name
class foo('Bar')['Baz'].Spam
Spam = auto @ # even with expressions!
class
_Class = auto @ # an anonymous class
class foo('Bar')['Baz']
_Class = auto @ # no last .name
Determining the correct way to do this in the language of your choice (other than Babel, TypeScript, CoffeeScript, etc.) is left as an exercise for the reader. (Take a look at the code your language generates for a class statement, or if it has an online interactive "playground" the way Babel, TS, and CS do, even better!)
Return Value Handling
autocreate
respects the standard Javascript rules for constructor return values. If your wrapped constructor returns an object or function, it will be returned in place of the newly-created instance -- regardless of whether the wrapper was called with new
or not.
On the other hand, if you supply a __class_call__
method, its return value will always be returned from the wrapper, regardless of type. This allows the class to behave like a normal function, when it's called as one.
(Of course, if you want your __class_call__
to return an instance, you can create one via new this.constructor(...)
, because this
in a __class_call__
is equal to the class's .prototype
.)
Inheritance
If you create a subclass of an autocreate
class, you should make it autocreate
as well. This is because it's impossible for a constructor to know whether it's being called directly, or via a subclass super
call. Thus, even though the base class can tell it wasn't invoked with new
, it can't tell which class it should create an instance of!
Also, you should be aware that since __class_call__
is a normal instance method, it is automatically inherited by subclasses. If you don't want the subclasses to respond to it, you can override the method in the subclasses, or you can write the method like this (Babel/ES7):
@auto class BaseClass {
__class_call__() {
if (this !== BaseClass.prototype) {
return new this.constructor(...arguments)
}
}
}
@auto class Subclass extends BaseClass {
}
This will then return a Subclass
instance when you call Subclass()
without new
, but fall through to whatever special handling you've set up when you call BaseClass()
without new
.