0

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.

3 Answers 3

1

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;
    }
  }
);

Sign up to request clarification or add additional context in comments.

Comments

0

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;
        }
      }
    },
  }
);

DEMO

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.

Comments

0

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...)

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.