Search code examples
typescript

Assigning values to elements of an object with different types


I earlier had problems with this first loop because of problems iteration keys, this is now solved with the following code. The two types, PopupPopsDefined and PopupProps are the same except that PopupProps allows undefined. This fixes problems I had with keys the other element would not accept. The code I have here all runs correctly, I just want to do the last part a better way without the case statements and the hard-coded assignments.

mergeTool(baseConfig: PopupPropsDefined, overrides: PopupProps): PopupPropsDefined {
    const keys = Object.keys(overrides) as (keyof PopupProps)[];
    keys.forEach((key) => {
      const newValue = overrides[key];
      if (typeof newValue !== 'undefined') {
        this.setPropValue(key, baseConfig, newValue);
      }
    });
    return baseConfig;
  }

The types are defined as:

export interface PopupProps {
  arrowDistance?: number;
  arrowHeight?: number;
  arrowOffset?: number;
  arrowWidth?: number;
  cornerRadius?: number;
  paddingX?: number;
  paddingY?: number;
  shiftX?: number;
  shiftY?: number;
  zIndex?: number;
  isManual?: boolean;
  isModal?: boolean;
  isTooltip?: boolean;
  logging?: boolean;
  debugMode?: boolean;
  direction?: PopupDirection;
  eventOff?: keyof HTMLElementEventMap;
  eventOn?: keyof HTMLElementEventMap;
  positioner?: string;
}

export interface PopupPropsDefined {
  arrowDistance: number;
  arrowHeight: number;
  arrowOffset: number;
  arrowWidth: number;
  cornerRadius: number;
  paddingX: number;
  paddingY: number;
  shiftX: number;
  shiftY: number;
  zIndex: number;
  isManual: boolean;
  isModal: boolean;
  isTooltip: boolean;
  logging: boolean;
  debugMode: boolean;
  direction: PopupDirection;
  eventOff: keyof HTMLElementEventMap;
  eventOn: keyof HTMLElementEventMap;
  positioner: string;
}

The following is the ugly code I want to do in a better way. Is there some way to detect the type of the object I'm copying a value to, so that I don't have to have switch statement like this. Its partly because it is so unmaintainable, if I add a new property I have to remember to add it here as well.

  setPropValue(key: string, config: PopupPropsDefined, value: number | boolean | string) {
    switch (key) {
      case 'arrowDistance':
      case 'arrowHeight':
      case 'arrowOffset':
      case 'arrowWidth':
      case 'cornerRadius':
      case 'paddingX':
      case 'paddingY':
      case 'shiftX':
      case 'shiftY':
      case 'zIndex':
        config[key] = <number>value;
        break;
      case 'isModal':
      case 'isTooltip':
      case 'logging':
      case 'debugMode':
      case 'isManual':
        config[key] = <boolean>value;
        break;
      case 'direction':
        config.direction = value as PopupDirection;
        break;
      case 'eventOff':
      case 'eventOn':
        config[key] = value as keyof HTMLElementEventMap;
        break;
      case 'positioner':
        config.positioner = <string>value;
        break;
    }
  }

Solution

  • The problem with your version of setPropValue() is that there's no guarantee that key and value have anything to do with each other; key can be any string at all, and value can be any number | boolean | string. Meaning you could call setPropValue("oopsiePoopsie", 123) or setPropValue("arrowDistance", "whoopsieDoodle") and the compiler would allow it. That forces you inside the function to all kinds of checks and type assertions to try to avoid and suppress errors.

    Given that at runtime all of your code inside setPropValue() is just doing config[key] = value, we should look for a solution that contains just that line. The way to do that is to make setPropValue() generic in K, the type of key, which should be constrained to the keys of PopupPropsDefined, and then the type of value should be the type of the property you get when you index into a PopupPropsDefined with a key of type K.

    Like so:

    setPropValue<K extends keyof PopupPropsDefined>(
        key: K,
        config: PopupPropsDefined,
        value: PopupPropsDefined[K]
    ) {
        config[key] = value; // okay
    }
    

    That compiles without error because the assignment config[key] = value has the indexed access type PopupPropsDefined[K] on both sides. The type of value is declared to be of that type, while the type of config[key] is that type because you're indexing into a PopupPropsDefined with a key of type K.

    And now it behaves as expected from the caller's side as well:

    x.setPropValue("arrowDistance", config, 1); // okay
    x.setPropValue("oopsiePoopsie", config, 123); // error!
    // ----------> ~~~~~~~~~~~~~~~
    // Argument of type '"oopsiePoopsie"' is not assignable 
    // to parameter of type 'keyof PopupPropsDefined'.
    x.setPropValue("arrowDistance", config, "whoopsieDoodle");
    // -----------------------------------> ~~~~~~~~~~~~~~~~
    // Argument of type 'string' is not assignable 
    // to parameter of type 'number'.
    

    Playground link to code