I use a Proxy
object with the idea that whenever a property gets updated, some other side-effect can also be initiated. I don't want to initiate the side effects in the many places that properties get set (DRY principle).
Somewhat contrived example code:
const session = new Proxy(
{ id: undefined as number, age: undefined as number}, // =target
{
set: (target, property, value): boolean => {
switch (property) {
case 'id': {
target[property] = value;
this.notifyIdWasUpdated(value);
return true;
}
case 'age': {
target[property] = value;
this.updateAgeDisplay(value);
return true;
}
default: {
return false;
}
}
}
}
);
My problem is that when I use my IDE's refactoring to change a property name (key) of the target
object (e.g. age
), the string constants in the case
statements (e.g. 'age'
) don't get updated as well (potentially causing a bug).
Question: Is there a way to dynamically get a string value 'key'
from an expression obj.key
in the case
statement (which would then be refactoring proof)? (Sort of the inverse of the ['key']
accessor, in a way...) Alternatively, can you suggest another way to structure the above code to guard against this sort of programmer oversight?
- I have found Get object property name as a string, but wonder if there is a less "iffy" solution - IMHO the tradeoff between a potential problem and adding a lot of code to guard against it is not worth it. (Many techniques seem to iterate through all keys and match on either property type or value; these will not be safe enough.)
- Typescript's documentation seems to say that metadata emission for reflection-like use is not yet officially adopted. Also not worth it IMHO to add a whole experimental library just for this.
CodePudding user response:
You can try to use keyof
here.
interface Session {
id: number
age: number
}
const session1 = new Proxy(
{ id: 0, age: 0 } as Session,
{
set: (target, property: keyof Session, value): boolean => {
switch (property) {
case 'id': {
target[property] = value;
this.notifyIdWasUpdated(value);
return true;
}
case 'age': {
target[property] = value;
this.updateAgeDisplay(value);
return true;
}
default: {
return false;
}
}
}
}
);
This will not be renamed automatically, but typescript will show error if property in case
doesn't exist in Session
.
The following case should allow automatic rename:
interface Session {
id: number
age: number
}
type Handlers<Model> = {
[Key in keyof Model]: (newValue: Model[Key]) => void;
}
// Partial<Handlers<Session>> in case you don't want to handle each property
const handlers: Handlers<Session> = {
id: () => { },
age: () => { },
}
const session = new Proxy(
{ id: 0, age: 0 } as Session,
{
set: (target, property: keyof Session, value): boolean => {
const handler = handlers[property];
if (handler) {
handler(value)
return true;
}
return false;
}
}
);
CodePudding user response:
The simples solution would be something like this:
function nameof<TType>(selector: (t?: TType) => any) {
const match = selector
.toString()
.match(/=>\s*(?:[a-zA-Z0-9_$] \.?)*?([a-zA-Z0-9_$] )$/);
if (match) {
return match[1];
}
return undefined;
}
interface MyType {
id: any;
age: number;
}
const session = new Proxy(
{ id: undefined as number, age: undefined as number }, // =target
{
set: (target, property, value): boolean => {
switch (property) {
case nameof<MyType>((t) => t.id): {
target[property] = value;
this.notifyIdWasUpdated(value);
return true;
}
case nameof<MyType>((t) => t.age): {
target[property] = value;
this.updateAgeDisplay(value);
return true;
}
default: {
return false;
}
}
},
}
);
NOTE: Careful, if you target ES5! Arrow function is transpiled into regular function with return
so that regex will not work, you have to change the regex.
CodePudding user response:
Although another answer was chosen, the problem with the given sample code is on a somewhat higher level of abstraction. Since the session
object encapsulates a number of properties, the session
object should be handled as a unit and not the properties separately. (There is probably a code smell name or some other warning against this...)
The sample would then simply be:
session = new Proxy(
{ id: undefined, age: undefined}, // =target
{
set: (target, property, value): boolean => {
if (typeof property === 'string' && Object.keys(target).includes(<string>property)) {
target[property] = value;
doSideEffects(target);
return true;
} else {
return false;
}
},
}
);
This simplifies the handler in the Proxy.
(I'm the OP. In my case, it has now also simplified the side effect code considerably. I guess the rubber duck effect came into play...)