Neither JavaScript nor TypeScript has the specific feature you're looking for, but JavaScript does have Proxy
objects, which are extremely powerful and can be used for this. You'd need to implement the get
and set
trap handlers (at minimum), and have the Contact
constructor return a proxy rather than the instance itself. (If you return an object from the constructor, that overrides the default behavior of returning the newly-created instance.) The get
handler gets called on every property "read" on the proxy, and the set
handler gets called on every property "write" on the proxy:
// ==== The implementation
const rexAllDigits = /^\d+$/;
const contactProxyHandlers = {
get(target, key) {
if (rexAllDigits.test(key)) {
return target.address[key];
}
return target[key];
},
set(target, key, value) {
if (rexAllDigits.test(key)) {
return Reflect.set(target.address, key, value);
}
return Reflect.set(target, key, value);
}
};
class Contact {
constructor() {
this.address = ["", "", ""];
return new Proxy(this, contactProxyHandlers);
}
}
// ==== Usage:
const c = new Contact();
c[0] = "address 0";
console.log(c[0], c.address[0]); // "address 0", "address 0"
c.example = "not an index property";
console.log(c.example);
In that example, I'm redirecting all property gets and sets that use all-digits property names to the address
array on the Contact
object, but passing through requests for other properties directly to the object.
Your C# version made address
private. Soon you'll be able to use private fields to do that in JavaScript, too. Until then, if you want to make it private, you can use a WeakMap
to do that, like this:
// ==== The implementation -- private parts
// You'd keep these parts private, for instance in a module and not exported
const contactAddresses = new WeakMap();
const proxyTargets = new WeakMap();
const rexAllDigits = /^\d+$/;
const contactProxyHandlers = {
get(target, key) {
if (rexAllDigits.test(key)) {
return contactAddresses.get(target)[key];
}
return target[key];
},
set(target, key, value) {
if (rexAllDigits.test(key)) {
return Reflect.set(contactAddresses.get(target), key, value);
}
return Reflect.set(target, key, value);
}
};
// ==== The implementation -- public parts
// You'd make this available, for instance exporting it from the module
class Contact {
constructor() {
// Initialize the entry in the map. If/when this object is ready
// for garbage collection, the entry will get removed automatically.
contactAddresses.set(this, ["", "", ""]);
const p = new Proxy(this, contactProxyHandlers);
proxyTargets.set(p, this);
return p;
}
showAddresses() {
const t = proxyTargets.get(this);
if (!t) {
throw new Error("Invalid `this` value for Contact");
}
console.log(JSON.stringify(contactAddresses.get(t)));
}
}
// ==== Usage:
const c = new Contact();
c[0] = "address 0";
console.log(c[0]); // "address 0"
c.showAddresses(); // "address 0", "", ""
c.example = "not an index property";
console.log(c.example);
The version using private fields would also need the proxyTargets
WeakMap
. It would look like this:
// ==== The implementation
class Contact {
static #rexAllDigits = /^\d+$/;
static #contactProxyHandlers = {
get(target, key) {
if (Contact.#rexAllDigits.test(key)) {
return target.#address[key];
}
return target[key];
},
set(target, key, value) {
if (Contact.#rexAllDigits.test(key)) {
return Reflect.set(target.#address, key, value);
}
return Reflect.set(target, key, value);
}
};
static #proxyTargets = new WeakMap();
#address = ["", "", ""];
constructor() {
const p = new Proxy(this, Contact.#contactProxyHandlers);
Contact.#proxyTargets.set(p, this);
return p;
}
showAddresses() {
const t = Contact.#proxyTargets.get(this);
if (!t) {
throw new Error("Invalid `this` value for Contact");
}
console.log(JSON.stringify(t.#address));
}
}
// ==== Usage:
const c = new Contact();
c[0] = "address 0";
console.log(c[0]); // "address 0"
c.showAddresses(); // "address 0", "", ""
c.example = "not an index property";
console.log(c.example);