How to Implement Dynamic Getters/Setters in JavaScript

Is it possible to implement dynamic getters/setters in JavaScript?

This changed as of the ES2015 (aka "ES6") specification: JavaScript now has proxies. Proxies let you create objects that are true proxies for (facades on) other objects. Here's a simple example that turns any property values that are strings to all caps on retrieval, and returns "missing" instead of undefined for a property that doesn't exist:

"use strict";
if (typeof Proxy == "undefined") {
throw new Error("This browser doesn't support Proxy");
}
let original = {
example: "value",
};
let proxy = new Proxy(original, {
get(target, name, receiver) {
if (Reflect.has(target, name)) {
let rv = Reflect.get(target, name, receiver);
if (typeof rv === "string") {
rv = rv.toUpperCase();
}
return rv;
}
return "missing";
}
});
console.log(`original.example = ${original.example}`); // "original.example = value"
console.log(`proxy.example = ${proxy.example}`); // "proxy.example = VALUE"
console.log(`proxy.unknown = ${proxy.unknown}`); // "proxy.unknown = missing"
original.example = "updated";
console.log(`original.example = ${original.example}`); // "original.example = updated"
console.log(`proxy.example = ${proxy.example}`); // "proxy.example = UPDATED"

Creating dynamic getters and setters from a base object in javascript?

Use Proxy constructor. Look to Developer Mozilla Proxy Page

var dynamicObject = new Proxy({   TEST_FLAG: false,   FRUIT: 'banana',   ID: 11  },{  get:function(target,key){   console.log(`get ${key} value. value is: ${target[key]}`);   return target[key]  },  set:function(target,key,val){    console.log(`set ${key} value. old value is:${target[key]} new value is ${val}`)    target[key] = val;    return true;  }})
console.log(dynamicObject.ID);dynamicObject.ID = 25;

// Output is:/*get ID value. value is: 1111set ID value. old value is:11 new value is 25*/

How can I make dynamically generated getter and setters known to TypeScript's compiler?

There are two things standing in the way of your code working as-is.

The first is that you cannot declare class fields implicitly inside the constructor method body in TypeScript code. If you want a class to have a property, you will need to explicitly declare this property outside the constructor:

class ColorPalette {
foregroundColor: string; // <-- must be here
backgroundColor: string; // <-- must be here
borderColor: string; // <-- must be here
// ...

There's a declined suggestion at microsoft/TypeScript#766 asking for such in-constructor declarations, and an open-but-inactive suggestion at microsoft/TypeScript#12613 asking for the same, but for the foreseeable future, it's not part of the language.

The second problem is that, in TypeScript code, calling Object.defineProperty() in a constructor does not convince the compiler that the property in question is definitely assigned, so when you use the --strict compiler option, you'd need something like a definite assignment assertion to quiet the warnings:

class ColorPalette {
foregroundColor!: string; // declared and asserted
backgroundColor!: string; // ditto
borderColor!: string; // ditto
// ...

There's a suggestion at microsoft/TypeScript#42919 for the compiler to recognize Object.defineProperty() as initializing properties, but for now, it's also not part of the language.

If you're willing to write the name and type of each property twice, then you can get your code to work way you originally had it. If you aren't, then you need to do something else.


One possible way forward is to make a class factory function that produces class constructors from some input. You put some property descriptors (well, functions that return such descriptors) into the function, and out comes a class constructor which sets those properties. It could look like this:

function ClassFromDescriptorFactories<T extends object>(descriptors: ThisType<T> &
{ [K in keyof T]: (this: T) => TypedPropertyDescriptor<T[K]> }): new () => T {
return class {
constructor() {
let k: keyof T;
for (k in descriptors) {
Object.defineProperty(this, k, (descriptors as any)[k].call(this))
}
}
} as any;
}

The call signature means: for any generic T of an object type, you can pass in a descriptors object whose keys are from T, and whose properties are zero-arg functions that produce property descriptors for each property from T; and what comes out of the factory function has a construct signature that produces instances of type T.

The ThisType<T> is not necessary, but helps with inference in the case where you are defining descriptors in terms of other class properties. More below:

The implementation iterates through all the keys of descriptors when the constructor is called, and for each such key k, it defines a property on this with the key k, and the property descriptor that comes out when you call descriptors[k].

Note that the compiler cannot verify that the implementation matches the call signature, for the same reasons that it cannot verify your original example; we have not declared the properties and Object.defineProperty() has not been seen to initialize them. That's why the returned class has been asserted as any. This suppresses any warnings about the class implementation, so we have to be careful that the implementation and call signature match.

But anyway, once we have ClassFromDescriptorFactories(), we can use it multiple times.


For your color palette example, you can make a general-purpose colorDescriptor() function which takes an initValue string input, and which produces a no-arg function which produces a property descriptor with the validation you want:

function colorDescriptor(initValue: string) {
return () => {
let value = initValue;
return {
enumerable: true,
get() { return value },
set(hex: string) {
if (/^#[0-9a-f]{6}$/i.test(hex)) {
value = hex;
}
}
}
}
}

The value variable is used to store the actual string value of the color. The whole point of the indirection () => {...} is so that each instance of the eventual class has its own value variable; Otherwise, you'd end up having value be a static property of the class, which you don't want.

And now we can use it with ClassFromDescriptorFactories() to define ColorPalette:

class ColorPalette extends ClassFromDescriptorFactories({
foregroundColor: colorDescriptor("#cccccc"),
backgroundColor: colorDescriptor("#333333"),
borderColor: colorDescriptor("#aaaaaa"),
}) {
toString(): string {
return Object.values(this).map(c => c.toString().toUpperCase()).join(", ");
}
}

This compiles without error, and the compiler recognizes instances of ColorPalette as having string-valued properties at keys foregroundColor, backgroundColor, and borderColor, and at runtime these properties have the proper validation:

let redPalette = new ColorPalette();

redPalette.foregroundColor = "#ff0000";
console.log(redPalette.toString()); // "#FF0000, #333333, #AAAAAA"

redPalette.backgroundColor = "oopsie";
console.log(redPalette.backgroundColor) // still #333333

And just to make sure each instance has its own properties, let's create a new instance:

let bluePalette = new ColorPalette();
bluePalette.foregroundColor = "#0000ff";
console.log(redPalette.foregroundColor) // #ff0000
console.log(bluePalette.foregroundColor) // #0000ff

Yeah, bluePalette and redPalette don't share a common foregroundColor property. Looks good!


Note the ThisType<T> code comes in handy in situations like this, where we are adding a new descriptor that refers to other properties of the class:

class ColorPalette extends ClassFromDescriptorFactories({
foregroundColor: colorDescriptor("#cccccc"),
backgroundColor: colorDescriptor("#333333"),
borderColor: colorDescriptor("#aaaaaa"),
foregroundColorNumber() {
const that = this;
const descriptor = {
get() {
return Number.parseInt(that.foregroundColor.slice(1), 16);
}
}
return descriptor;
}
}) {
toString(): string {
return Object.values(this).map(c => c.toString().toUpperCase()).join(", ");
}
}

Here the compiler understands that foregroundColorNumber() is defining a number property on T, and that the this inside the implementation corresponds to T, and thus calls like the following work without error:

console.log("0x" + redPalette.foregroundColorNumber.toString(16)) // 0xff0000
console.log(redPalette.foregroundColor = "#000000")
console.log("0x" + redPalette.foregroundColorNumber.toString(16)) // 0x0

If you remove ThisType<T>, you will see some errors show up.

Playground link to code

dynamic getter and setters - a possibility

But unfortunately the undefined property of window is configurable: false

This is true only since EcmaScript 5.1. Before, it was overwritable.

what if I would use the original window.undefined getter and setter, after all it must be called every time I screw up and misspell a word or something.

No, it would not have worked. There is a difference between the undefined value and the global variable "undefined". The variable is not evaluated each time a undefined value is encountered (e.g. in typeof (void 0)), only when you use it explicitly (such as in g.someprop === undefined).

Any ideas how to solve the dynamic getters and setters problem?

There is only one solution: Proxies. Unfortunately, it is only a harmony draft and currently only supported in Firefox' Javascript 1.8.5.

See also Is there an equivalent of the __noSuchMethod__ feature for properties, or a way to implement it in JS?, Detect when a new property is added to a Javascript object? or How to detect when a property is added to a JavaScript object? which implements a polling solution (checking for changes via setInterval).

For a clean solution, you currently have to use an explicit getter function, which you pass the property name (g.get("someundefinedproperty")).

Using getter / setter for dynamic object properties

If you have a list of these, just create the getters/setters in a loop, e.g.:

this.status = {};
["isOnFire", "hasPinkShirt"].forEach((name) => {
Object.defineProperty(status, name {
get() {
console.log(_statusChanged);
return _status[name];
},
set(val) {
_status[name] = val;
_statusChanged[name] = new Date();
return _status[name];
}
});
});

If they could be anything, then you'll want to use a Proxy object. With a proxy, you can capture all gets/sets without knowing property names in advance:

this.status = new Proxy(_status, {
get(target, propKey, receiver) {
// handle get
return _status[propKey];
},
set(target, propKey, value, receiver) {
// handle set
_status[propKey] = value;
_statusChanged[propKey] = new Date();
return true; // Tells the proxy the assignment worked
}
});

(Or you might use Reflect.get and Reflect.set, but even Firefox doesn't have them yet.)

Here's an article going into proxies in more detail.

Here's an example, but you'll need to run it in a recent version of Firefox because support or Proxy in the wild is still really thin on the ground, and by their nature, you can't shim/polyfill proxies.

(function() {  "use strict";
var _status = {}; var _statusChanged = {}; var status = new Proxy(_status, { get(target, propKey, receiver) { snippet.log(propKey + " requested"); return _status[propKey]; }, set(target, propKey, value, receiver) { snippet.log(propKey + " set to " + value); _status[propKey] = value; _statusChanged[propKey] = new Date(); return true; // Tells the proxy the assignment worked } }); status.foo = "bar"; snippet.log("foo = " + status.foo);
})();
<!-- Script provides the `snippet` object, see http://meta.stackexchange.com/a/242144/134069 --><script src="http://tjcrowder.github.io/simple-snippets-console/snippet.js"></script>

Is it possible to create dynamic getters/setters in typescript?

thanks to the https://github.com/epicgirl1998, she helped me to find the solution. I'll post it here:

the error is that the getter has a value parameter even though getters
aren't passed any value

i replaced it with get: function() { return this.data[key]; }, and now
the only error is that there's a super call in the class which is only
needed if the class extends another class

also, this inside the accessors doesn't refer to the class instance,
but using arrow functions for them should fix it

try this:

interface IData {
id: number;
[propName: string]: any;
}

class Model {

protected updatedKeys:string[] = [];

baseUrl:string = null;
data:IData;
fields:IData;

constructor(data:IData={id:null}, fields:IData={id:null}) {

this.data = data;
this.fields = fields;

for(let key in this.data) {
Object.defineProperty(this, key, {
get: () => { return this.data[key]; },
set: (value:any) => {
if (this.data[key] !== value) {
this.data[key] = value;
this.updatedKeys.push(key);
}
},
});
}
}
}

Dynamic getters and settings for classes in TypeScript

The cleanest solution I could find is the following:

interface UserProps {
name?: string;
age?: number;
}

export class User {
constructor(private data: UserProps) {}

// Add generic magic here
get<K extends keyof UserProps>(propName: K): UserProps[K] {
return this.data[propName];
}

set(update: UserProps): void {
Object.assign(this.data, update);
}
}

Alternatively, you can add an arbitrary key indexing as well as update the type for your get statement.

interface UserProps {
name?: string;
age?: number;
// this next line is important
[key: string]: string | number | undefined;
}

export class User {
constructor(private data: UserProps) {}

// number|string changed to number|string|undefined
get(propName: string): number | string | undefined {
return this.data[propName];
}

set(update: UserProps): void {
Object.assign(this.data, update);
}
}

Javascript dynamically getter/setter for private properties

The getter/setters have to be in the scope that can see the private variables and the only scope that can see these variables is the internals of the constructor. That's why these variables are actually private. So, to make setters/getters for them, you have to put the functions in that scope that can see them. This will work:

function winClass (posX, posY, w, h) {
var x = posX || 0;
var y = posY || 0;
var width = w || 0;
var height = h || 0;

this.getX = function() {return(x);}
this.setX = function(newX) {x = newX;}
}

var win1 = new winClass (10, 10, 100, 100);
alert(win1.getX()); // alerts 10

You can see it work here: http://jsfiddle.net/jfriend00/hYps2/.

If you want a generic getter/setter for privates, you could do it like this:

function winClass (posX, posY, w, h) {
var privates = {};
privates.x = posX || 0;
privates.y = posY || 0;
privates.width = w || 0;
privates.height = h || 0;

this.get = function(item) {return(privates[item]);}
this.set = function(item, val) {privates[item] = val;}
}

var win2 = new winClass(10,10,100,100);
alert(win2.get("x")); // alerts 10

And, if you want to hack around the private nature of these variables which makes no sense to me (as you might as well make them standard instance variables then), you can do it like this:

function winClass (posX, posY, w, h) {
var privates = {};
privates.x = posX || 0;
privates.y = posY || 0;
privates.width = w || 0;
privates.height = h || 0;

this.getPrivates = function() {return(privates);}
}

winClass.prototype.getX = function() {
return(this.getPrivates().x);
}

winClass.prototype.setX = function(newX) {
this.getPrivates().x = newX;
}

Example here: http://jsfiddle.net/jfriend00/EKHFh/.

Of course, this ruins the private nature of the variables so there isn't really any point in doing it this way as making them regular instance variables would be easier and have the same access control.

And, for completeness, here's the normal instance variable method that freely lets you add accessor methods to the prototype, but the variables aren't private.

function winClass (posX, posY, w, h) {
this.x = posX || 0;
this.y = posY || 0;
this.width = w || 0;
this.height = h || 0;
}

winClass.prototype.getX = function() {
return(this.x);
}

winClass.prototype.setX = function(newX) {
this.x = newX;
}

How to generate getters and setters in Javascript?

You can use a proxy with get and set traps, use TS types to allow only props you wish to handle (TS playground)):

interface Flags {
logged: boolean,
'notifications-muted': boolean;
}

type Prop = keyof Flags;

const handlers = {
get(_: Flags, prop: Prop) {
return localStorage.getItem(prop) === "true";
},

set(_: Flags, prop: Prop, val: any) {
if (val) {
localStorage.setItem(prop, "true");
} else {
localStorage.removeItem(prop);
}

return true;
}
};

const flags = new Proxy<Flags>({} as Flags, handlers);


Related Topics



Leave a reply



Submit