Skip to content

Instantly share code, notes, and snippets.

@phpnode
Created January 19, 2018 18:25
Show Gist options
  • Save phpnode/aef78181987b58fff26d4daf8cb09bab to your computer and use it in GitHub Desktop.
Save phpnode/aef78181987b58fff26d4daf8cb09bab to your computer and use it in GitHub Desktop.
Extension methods for JS using proxies!
// @flow
const proxyCache = new WeakMap();
const extensionMethods = new WeakMap();
function extended<T>(instance: T): $Supertype<T> {
if (isPrimitive(instance)) {
return instance;
}
const existingProxy = proxyCache.get(instance);
if (existingProxy !== undefined) {
return existingProxy;
}
const proxy = createProxy((instance: $FlowFixMe));
proxyCache.set(instance, proxy);
return proxy;
}
function getType<T: Function | Object>(input: T) {
if (input.constructor == null) {
return input;
}
if (input.prototype && input.prototype.constructor === input) {
return input;
}
return input.constructor;
}
function isPrimitive(input) {
return (
(typeof input !== "object" && typeof input !== "function") || input === null
);
}
function createProxy<T: Function | Object>(instance: T) {
const type = getType(instance);
function getMethod(prop: string, kind: string) {
const methods = extensionMethods.get(type);
if (
methods !== undefined &&
methods[prop] !== undefined &&
typeof methods[prop][kind] === "function"
) {
return methods[prop][kind];
}
}
return new Proxy(instance, {
has(target: T, prop: $Keys<T>) {
const method = getMethod(prop, "get");
if (method === undefined) {
return prop in (target: $FlowFixMe);
}
return method();
},
get(target: T, prop: $Keys<T>, receiver: *) {
const method = getMethod(prop, "get");
if (method === undefined) {
return extended(Reflect.get(target, prop, receiver));
}
return extended(method.call(receiver));
},
set<P: $Keys<T>>(
target: T,
prop: P,
value: $ElementType<T, P>,
receiver: *
) {
const method = getMethod(prop, "get");
if (method === undefined) {
return Reflect.set(target, prop, value, receiver);
} else {
return extended(method.call(receiver, value));
}
}
});
}
function defineExtension(owner, propertyName, definition) {
const type = getType(owner);
let methods = extensionMethods.get(type);
if (methods === undefined) {
methods = {};
extensionMethods.set(type, methods);
}
methods[propertyName] = definition;
return owner;
}
class User {
name: string;
roles: Role[];
isAdmin() {
for (const role of this.roles) {
if (role.name === "admin") {
return true;
}
}
return false;
}
}
class Role {
name: string;
}
const ext = extended;
console.clear();
const guest = new Role();
guest.name = "guest";
const user = new User();
user.name = "Whatever";
user.roles = [guest];
const u = ext(user);
console.log("user namm", u.name);
console.log("role name", u.roles[0].name);
console.log("user is admin", u.isAdmin());
console.log("applying extensions....");
defineExtension(User, "name", {
get() {
return "Bob";
}
});
defineExtension(Role, "name", {
get() {
return "admin";
}
});
console.log("user namm", u.name);
console.log("role name", u.roles[0].name);
console.log("user is admin", u.isAdmin());
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment