-
-
Save bakkot/6924e788ac954fe8302e1c8db429a4af to your computer and use it in GitHub Desktop.
Descriptors
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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