Home > Software design >  Reflection-like conversion of key to a string in TypeScript?
Reflection-like conversion of key to a string in TypeScript?

Time:02-19

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

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.

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

  • Related