Home > OS >  Is it possible to have type inference for a param based on the other param passed to a function?
Is it possible to have type inference for a param based on the other param passed to a function?

Time:11-27

Explanation

I'm working with react-select and want to generate the options array which would be passed to the react-select component.

The options array is of the type:

type TOptions = { value: string; label: string }[]

I get the data for the options array from an API. The data will have a structure like:

{
  name: string;
  slug: string;
  id: number;
}[]

So, I created a helper function to transform the data into the options array format.

const generateSelectOptions = (
  data: Record<string, string>[],
  field: { valueKey: string; labelKey: string }
) => {
  const options = data.map((data) => ({
    value: data[field.valueKey],
    label: data[field.labelKey],
  }));

  // TODO: create a better type check
  return options as TOptions;
};

This function will have get two params,

  1. data - data received from the API
  2. field - which is an object that would contain the keys, valueKey and labelKey from the data object that would be mapped appropriately to the options array.

The function I created works fine, but I have manually asserted the return type of the function as TOptions.

Example

  const data = [
    {
      name: "Holy Holy Holy",
      slug: "holy-holy-holy",
      id: 1,
    },
    {
      name: "Amazing Grace",
      slug: "amazing-grace",
      id: 2,
    },
  ];

  const options = generateSelectOptions(data, {
    valueKey: "slug",
    labelKey: "name",
  });

  // now options would be
  options = [
    {
      label: "Holy Holy Holy",
      value: "holy-holy-holy",
    },
    {
      label: "Amazing Grace",
      value: "amazing-grace",
    },
  ];

Question

Now, I'm thinking of a better way to type generateSelectOptions function, where when calling the function, as soon as I give the first argument data, the fieldKeys object which would be the second argument should automatically get type inference, as the fieldKeys - valueKey and labelKey can only be of the type keyof data

Is there a way to achieve this? I would really appreciate some help here. Thanks

CodePudding user response:

Sure, use a generic type parameter which will be inferred from the first argument, the second argument will then be validated against it:

function getOptions<T>(
  data: T[], 
  field: { valueKey: keyof T }
) { /*...*/ }

CodePudding user response:

type TOptions = { value: string; label: string}[];

function generateSelectOptions<T extends Record<string, unknown>, KeyField extends keyof T, LabelField extends keyof T>(data: T[], field: {valueKey: KeyField, labelKey: LabelField}): TOptions {
  return data.map(entry => ({
    label: String(entry[field.labelKey]),
    value: String(entry[field.valueKey])
  })) 
}


 const data = [
    {
      name: "Holy Holy Holy",
      slug: "holy-holy-holy",
      id: 1,
    },
    {
      name: "Amazing Grace",
      slug: "amazing-grace",
      id: 2,
    },
  ];

  const options: TOptions = generateSelectOptions(data, {valueKey: 'id', labelKey: 'name'});

Obviously, without casting to a string, the values of your data record contain arbitrary values (e.g. strings and numbers). If you only want the label to be a number, you need to cast everything you might find there to a string (e.g. with String(value).

CodePudding user response:

You could use generics, create a type for your data and pass it into your generateSelectOptions function.

You would still need to cast values that you will get from data[field.valueKey] and data[field.labelKey] to a string to match TOptions type

This is what I came up so far and if you will try to replace valueKey and labelKey values to something else rather than slug or name or id your IDE will give you a type hint

type TOption = { value: string; label: string };

const generateSelectOptions = <T>(
  data: T[],
  field: { valueKey: keyof T; labelKey: keyof T }
): TOption[] => {
  const options = data.map<TOption>((data) => ({
    value: data[field.valueKey] as string,
    label: data[field.labelKey] as string,
  }));

  return options;
};

type DataType = { name: string; slug: string; id: number };

const data: DataType[] = [
  {
    name: "Holy Holy Holy",
    slug: "holy-holy-holy",
    id: 1,
  },
  {
    name: "Amazing Grace",
    slug: "amazing-grace",
    id: 2,
  },
];

const options = generateSelectOptions<DataType>(data, {
  valueKey: "slug",
  labelKey: "name",
});

console.log(options);

you could even add some extra layer of validation on top of the options that gets generated using type guard, but I think that should not be neccessary

const isOptionGuard = (option: any): option is TOption => "value" in option && "label" in option;

const isValidType = options.every(isOptionGuard);

console.log(isValidType)

if (!isValidType) throw Error("GenerateSelectOptions failed");
  • Related