Typescript - How to dynamically type an object with keys from a dynamic array of objects

Viewed 5207

I have a function that reduces an array of properties and builds an object:

type FieldProps = {
  name: string;
  value: string;
  required: boolean;
  styles?: object;
  className?: string
}

const fields: FieldProps[] = [
  {
    name: "firstName",
    value: "Bob",
    required: true,
    styles: { width: "100%"}
  },
  {
    name: "lastName",
    value: "Smith",
    required: true,
    className: "example"
  },
  {
    name: "city",
    value: "Tulsa",
    required: true,
  },
  {
    name: "state",
    value: "OK",
    required: true,
  },
]

const parseFields = <T extends any[], K extends object = {}>(fields: T): K => {
  try {
    if (!fields || (fields && fields.length < 0)) throw new Error("You must supply an array of fields!");

    const parsedFields = fields.reduce((acc, { name, value }: { name: string, value: string}) => {
      switch (name) {
        case "city":
        case "street":
        case "state":
        case "suite":
        case "zipCode": {
          acc["address"] = acc["address"] || {};
          if (value) acc.address[name] = value;
          break;
        }
        default: {
          acc[name] = value;
          break;
        }
      }
      return acc;
    }, {} as K);

    return parsedFields;
  } catch (err) {
    throw String(err);
  }
};

const userDetails = parseFields(fields);

As of now, hovering over userDetails is displaying an empty object type({}), when it ideally should be:

{
  "firstName": string,
  "lastName": string,
  "address": {
    "city": string,
    "state": string
  }
}

How can I refactor the function to take the type FieldProps[] and have it return a dynamically typed object?

Typescript playground


Update

It's super close to being typed with the help of @Mingwei, but I'd essentially like to return the typeof ParseFieldsResult<T>. For example, I tweaked his playground and I'd like to return the typeof O. What's silly is that if I copy & move the type ParseFieldsResult<T> into the return, then it gives me the object type I'd expect see on userDetails (but with WET code): playground.

3 Answers

On the parseFields function, you have a return type of K.. which is an empty object... shouldn't you be passing in FieldProps?

const parseFields = <T extends any[], K extends object = {}>(fields: T): **FieldProps** => {

You are using a generic method here, and also setting K's default type to {}, that's why you are seeing {} during hover over.

You can achieve what you are looking for by calling the method properly. Like this:

const userDetails = parseFields<FieldProps[], {
  "firstName": string,
  "lastName": string,
  "address": {
    "city": string,
    "state": string
  }
}>(fields);  

This answer is very (over)complicated, but you do end up with this type:

const userDetails = parseFields(fields);
type Z = typeof userDetails;
// type Z = {
//     address: {
//         city: "Tulsa" | "Norman"; // Added to test
//         state: "OK";
//     };
// } & {
//     firstName: "Bob";
//     lastName: "Smith";
// }

It requires making fields as const:

const fields = [
  ...
] as const;

And... the actual typings:

type AddressKeys = 'city' | 'street' | 'state' | 'suite' | 'zipCode';

type _ParseFieldsResultKeys<T extends readonly FieldProps[]> = {
  [K in keyof T]: T[K] extends { name: string } ? T[K]['name'] : never;
}[Exclude<keyof T, keyof []>];

type _GetParseFieldResultValue<T extends readonly FieldProps[], N> =
  Extract<T[keyof T], { name: N }> extends { value: infer V } ? V : never;

type ParseFieldsResult<T extends readonly FieldProps[]> = {
  address: {
    [N in Extract<_ParseFieldsResultKeys<T>, AddressKeys>]: _GetParseFieldResultValue<T, N>;
  }
} & {
  [N in Exclude<_ParseFieldsResultKeys<T>, AddressKeys>]: _GetParseFieldResultValue<T, N>;
};

const parseFields = <T extends readonly FieldProps[]>(fields: T): ParseFieldsResult<T> => {

It's probably not a good idea to use this. But: Playground Link


Edit: realized we don't need the actual string literal types, with just string values we can get rid of _GetParseFieldResultValue and still get:

type Z = {
    address: {
        city: string;
        state: string;
    };
} & {
    firstName: string;
    lastName: string;
}

Playground Link

Related