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,
data
- data received from the APIfield
- which is an object that would contain the keys,valueKey
andlabelKey
from thedata
object that would be mapped appropriately to theoptions
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");