Skip to content

Instantly share code, notes, and snippets.

@bakkot
Last active August 5, 2017 21:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bakkot/6924e788ac954fe8302e1c8db429a4af to your computer and use it in GitHub Desktop.
Save bakkot/6924e788ac954fe8302e1c8db429a4af to your computer and use it in GitHub Desktop.
Descriptors
// This defines a pair of decorators, `@inheritable` and `@inherited`, which can be used for protected (subclass-only) accessibility
// See https://github.com/littledan/proposal-unified-class-features
let classes = new WeakMap(); // class -> field name -> { getter, setter }
function inheritable(descriptor) { // stash accessors for the decorated property
let { key } = descriptor;
if (!(key instanceof PrivateName)) {
throw new TypeError('only private fields can be @inheritable');
}
descriptor.finisher = ctor => {
if (!classes.has(ctor)) classes.set(ctor, new Map());
let nameMap = classes.get(ctor);
nameMap.set(String(key), {
getter(o) {
return key.get(o);
},
setter(o, v) {
key.set(o, v);
}
});
};
return descriptor;
}
function inherited(descriptor) { // replace the decorated private field with the accessors stashed above
let { key } = descriptor;
if (!(key instanceof PrivateName)) {
throw new TypeError('only private fields can be @inherited');
}
if (descriptor.descriptor.initializer != null) {
throw new TypeError('@inherited fields can\'t have an initializer'); // this could probably be worked around
}
let getter, setter; // can't be set at definition time because the class isn't available
let finisher = ctor => {
let proto = ctor;
while ((proto = proto.prototype) != null) {
if (classes.has(proto)) {
let nameMap = classes.get(nameMap);
if (nameMap.has(String(key))) {
({ getter, setter } = nameMap.get(String(key)));
break;
}
}
}
if (getter === void 0) {
throw new TypeError('field marked as @inherited, but has no corresponding @inheritable field in a superclass');
}
};
return {
kind: 'method',
key,
finisher,
descriptor: {
get: function() { return getter(this); }, // this layer of indirection is unfortunately necessary
set: function(v) { setter(this, v); }
}
};
}
// Usage:
class Base {
#foo;
@inheritable
#bar = 1;
method() {
return this.#foo;
}
}
class Derived extends Base {
@inherited
#bar;
setBar(v) {
this.#bar = v;
}
}
let derived = new Derived();
derived.setBar(2);
derived.method(); // 2
class BadDerived extends Base {
@inherited // fails, '#foo' is not marked as inheritable
#foo;
@inherited
#baz; // fails, Base has no '#baz'; this error is not distinguishable from the previous
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment