1

ReactJS and TypeScript. My overall objective is to create a single event handler function that takes in the name of the input element (select, input, etc) and sets the property of a complex object based on the property name along with the value from the event.

In essence, something like this:

  handleChange = idx => e => {
    const { name, value } = e.target;
    const rows = [...this.state.rows];
    rows[idx] = {
      [name]: value
    };
    this.setState({
      rows
    });
  };

However, in my case, I'm using TypeScript, and I'm also using a fairly complex object.

Here is some test code:(reference Using Type Parameters in Generic Constraints)

export interface ITraveler {
  id: number;
  dob: Date;
  tripCost: number;
  firstName: string;
  lastName: string;
}

export enum CoverageType {
  Standard,
  Plus,
}

export default interface IQuoteInfo {
  country: string;
  state: { fullname: string; abbreviation: string };
  travelDates: {
    fromDate: Date;
    toDate: Date;
  };
  travelerInfo: ITraveler[];
  paymentInfo: {
    initialDate: Date;
    finalDate: Date | null;
  };
  coverageType: CoverageType;
  tripInfo: {
    name: string;
    program: string;
  };
  contactInfo: {
    firstName: string;
    lastName: string;
    address1: string;
    address2: string;
    city: string;
    state: string;
    zip: string;
    country: string;
    phone: string;
    email: string;
  };
  setQuoteInfo: (e: any) => void;
}

const initialQuoteInfo: IQuoteInfo = {
  country: "USA",
  state: {
    fullname: "New York",
    abbreviation: "NY",
  },
  travelDates: {
    fromDate: new Date(2020, 10, 28),
    toDate: new Date(2020, 10, 30),
  },
  travelerInfo: [
    { id: 1, dob: new Date(), tripCost: 0, firstName: "", lastName: "" },
  ],
  paymentInfo: {
    initialDate: new Date(),
    finalDate: null,
  },
  coverageType: CoverageType.Standard,
  tripInfo: {
    name: "",
    program: "Program Name",
  },
  contactInfo: {
    firstName: "",
    lastName: "",
    address1: "",
    address2: "",
    city: "",
    state: "",
    zip: "",
    country: "",
    phone: "",
    email: "",
  },
  setQuoteInfo: () => {},
};

const getProperty = <T, K extends keyof T>(obj: T, key: K) => {
  return key;
};

const setProperty = <T, K extends keyof T>(obj: T, key: K, value: any) => {
  obj[key] = value;
  console.log("object is", obj);
};

setProperty(initialQuoteInfo, "country", "United Kingdom");

Okay, so what's my problem? This works, but I'm at a loss as to how I'd set the property and value for anything deeper or more complex than a key/value property. For example, how would I set the state fullname and abbreviation? Assuming that the element "name" would be "state.fullname", I would try to do this:

setProperty(initialQuoteInfo, "state.fullName", "Virginia");

TypeScript complains:

Argument of type '"state.fullName"' is not assignable to parameter of type '"contactInfo" | "country" | "state" | "travelDates" | "travelerInfo" | "paymentInfo" | "coverageType" | "tripInfo" | "setQuoteInfo"'.ts(2345)

This works:

setProperty(initialQuoteInfo, "state", {
  fullname: "Virginia",
  abbreviation: "VA",
});

There are a couple of ways I can think of to manage this off-hand. One would be to parse the "name" and if it has a dot, then do some kind of funky setting. Another might be to create another function that takes in the parsed name and does essentially the same thing as the setProperty function.

I can't help but think there might be a more elegant and functional methodology, so I'd appreciate any assistance.

Dan7el
  • 1,995
  • 5
  • 24
  • 46
  • maybe this helps:https://stackoverflow.com/questions/58434389/typescript-deep-keyof-of-a-nested-object – Leo Oct 27 '20 at 14:36

1 Answers1

2
function setProperty(obj: object, key: string, value: any) {
    const propertyPath = key.split(".");
    const propertyName = propertyPath.pop();
    const baseObj = propertyPath.reduce((t: any, c) => t[c], obj);
    baseObj[propertyName] = value;
}

Tested with:

setProperty(initialQuoteInfo, "country", "United Kingdom");
setProperty(initialQuoteInfo, "state.fullname", "Virginia");
setProperty(initialQuoteInfo, "coverageType", CoverageType.Standard);
setProperty(initialQuoteInfo, "travelerInfo.0.dob", new Date(1980, 1, 1));

I'll admit it doesn't fulfil your 'elegant and functional' criteria, but it does what you want I think and is fairly easy to understand.

Rich N
  • 8,939
  • 3
  • 26
  • 33