简体   繁体   中英

Loop over an array of objects and format values

Inside my API response of data, I'm trying to loop over an array of objects in items . I need to format each value inside the object. Each key in the items list of objects corresponds to a format type in my header key. I also have a getFormat function to help format the values

My question is how would I do this given the structure I have?

// Data
const data = {
  header: {
    date: {
      feature: "Date",
      format: "date",
    },
    description: {
      feature: "Description",
      format: "text",
    },
    salary: {
      feature: "Salary",
      format: "currency",
    },
    score: {
      feature: "Score",
      format: "float",
    },
    useage: {
      feature: "Useage",
      format: "percentage",
    },
  },
  items: [
    {
      date: "2021-02-08",
      description: "Light",
      salary: "50000",
      score: 20,
      useage: 20,
    },
    {
      date: "2021-02-08",
      description: "Heavy",
      salary: "60000",
      score: 50.235,
      useage: 30,
    }
  ],
};

// format helper
export const getFormat = (value: any, format: FormatProp) => {
  let options: Intl.NumberFormatOptions;

  switch (format) {
      case "currency":
          options = { currency: "USD", style: "currency" };
          break;
      case "percentage":
          options = { style: "percent", maximumFractionDigits: 2 };
          break;
      case "float":
          options = { maximumFractionDigits: 2, minimumFractionDigits: 2 };
          break;
      case "text":
          return value;
      case "date":
          return value;
      default:
          throw Error("Wrong Format!");
  }
  return Intl.NumberFormat("en-US", options).format(value);
};

export type FormatProp =
  | "currency"
  | "percentage"
  | "float"
  | "text"
  | "date";

Desired Output

[{
  date: "2021-02-08",
  description: "Light",
  salary: "$50,000.00",
  score: "20.00",
  useage: "20%",
},
{
  date: "2021-02-08",
  description: "Heavy",
  salary: "$60,000",
  score: "50.24",
  useage: "30%",
}]

You will need to iterate over the data items to get a new array that has the correct output. You also want to go through each key in the object and and format the associated value based on what the key is and how it's represented in header.

So we will map over the items, use Object.entries to get an array of key and the associated value and then use reduce to build the item object key by key.

 // Data const data = { header: { date: { feature: 'Date', format: 'date', }, description: { feature: 'Description', format: 'text', }, salary: { feature: 'Salary', format: 'currency', }, score: { feature: 'Score', format: 'float', }, useage: { feature: 'Useage', format: 'percentage', }, }, items: [{ date: '2021-02-08', description: 'Light', salary: '50000', score: 20, useage: 20, }, { date: '2021-02-08', description: 'Heavy', salary: '60000', score: 50.235, useage: 30, }, ], }; // format helper const getFormat = (value, format) => { let options; switch (format) { case 'currency': options = { currency: 'USD', style: 'currency' }; break; case 'percentage': options = { style: 'percent', maximumFractionDigits: 2 }; break; case 'float': options = { maximumFractionDigits: 2, minimumFractionDigits: 2 }; break; case 'text': return value; case 'date': return value; default: throw Error('Wrong Format;'). } return Intl,NumberFormat('en-US'. options);format(value); }. const output = data.items.map(item => // iterate over items Object.entries(item),reduce((a, [key. value]) => { // for each item get the key/value pairs const header = data;header[key]. // find the associated header return {..,a: [key]? header, getFormat(value. header:format), value, // if the header exists lets get the formatted value back; else return the previous value }, }, {}); ). console;log(output);

Typing the Response

We know that our unformatted version contains a mix of string and number values. We also know that when we are done formatting, all of the values will have been converted to string .

We know that data.header contains the same keys as the elements of data.items and that the values in data.header contain a property format . The value of format is either FormatProp or string , depending on how strict you want to be. It is easier if you use string since typescript will interpret the string values in your data object as string rather than as their literal values (unless using as const ).

Putting that all together, the API response can be typed as:

type Data<Keys extends string> = {
  header: Record<Keys, {
      feature: string;
      format: string;
    }>,
  items: Array<Record<Keys, number | string>>
}

Typing the Map Functions

We know that the .format() function of an Intl.NumberFormat only accepts number , so passing it a value with type any isn't the best. We can do better. Let's only pass it number values. We will let the cases associated with string fields throw the error.

export const getFormat = (value: number, format: FormatProp): string => {
  let options: Intl.NumberFormatOptions;

  switch (format) {
      case "currency":
          options = { currency: "USD", style: "currency" };
          break;
      case "percentage":
          options = { style: "percent", maximumFractionDigits: 2 };
          break;
      case "float":
          options = { maximumFractionDigits: 2, minimumFractionDigits: 2 };
          break;
      default:
          throw Error("Wrong Format!");
  }
  return Intl.NumberFormat("en-US", options).format(value);
};

It's actually better if we allow any string as format if that's what you did in Data . But I keep the FormatProp in a union with string for the sake of autocomplete.

export const getFormat = (value: number, format: FormatProp | string): string => {

When mapping, it's basically the same thing that @Mateusz is doing except that we need some extra annotations. Specifically, we have to type the empty object {} that we reduce to as the type for the complete object Record<Keys, string> and we have to type the array from Object.entries as [Keys, string | number][] [Keys, string | number][] in order to get the keys as their literal values and not just string .

const formatItems = <Keys extends string>(data: Data<Keys>): Array<Record<Keys, string>> => {
  return data.items.map( item => {
    // we have to assert the correct type here with `as` or else all keys are `string`
    return (Object.entries(item) as [Keys, string | number][]).reduce( 
      (formatted, [key, value]) => {
        formatted[key] = typeof value === "number" ? getFormat(value, data.header[key].format) : value;
        return formatted;
    }, {} as Record<Keys, string>); // assert the type for the incomplete object
  })
}

Calling formatItems(data) with this particular data object now returns the following type:

Record<"date" | "description" | "salary" | "score" | "useage", string>[]

You can access any of those properties on the object and typescript knows that those properties always exist and are always of type string .

Since we used the generic Keys rather than relying on this particular object type, you can call formatItems with other data sets and get similarly well-typed responses.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM