Skip to main content
Version: Next

Working with option lists

Refer to the TypeScript reference page for information about the types and interfaces referenced below.

Option list props in React Query Builder like fields, combinators, and operators, are based on the type OptionList. OptionList is a union type allowing two different types of arrays: Option[] and OptionGroup[]. This is a flexible design, but it adds some complexity to the consumption of option list-type props in custom subcomponents.

This page provides tips to help manage the inherent ambiguity of option list TypeScript types.

info
  • Option objects have label, name, and value properties, all of which extend string.
  • OptionGroup objects have a label property along with an options property which is an array of Option objects.

This structure is similar to the children prop on a <select> element, which can be an array of <option> elements or an array of <optgroup> elements that each contain its own nested <option> list.

An incorrect assumption

You may have found this page after seeing a TypeScript error message similar to this:

Property 'name' does not exist on type 'FullOption<string> | OptionGroup<FullOption<string>>'.
Property 'name' does not exist on type 'OptionGroup<FullOption<string>>'. ts(2339)

This generally happens when you treat elements in the option list as if they were definitely Option types. For example, mapping through the list to get the name property, like this:

const ListAllOptionNames = (props: ValueSelectorProps) => {
return <div>{props.options.map(opt => opt.name).join(', ')}</div>;
// ^^^^ error
};

It's natural to assume that an option list passed in to QueryBuilder as an Option[] would still be an Option[] when it gets to the subcomponent. However, React Query Builder's type generics only infer the type of the options within the list, not the type of the list itself.

tip

All options in list-type props passed to subcomponents are guaranteed to include both name and value properties, even if they don't include one or the other in their original form on the corresponding QueryBuilder prop.

As an example, consider this fields array:

const fields: Field[] = [
{ name: 'firstName', label: 'First Name' },
{ name: 'lastName', label: 'Last Name' },
];

When this array is assigned to the fields prop, the fieldSelector component will receive the same array but with each option object augmented with a value property equivalent to the original name:

const MyFieldSelector = (props: FieldSelectorProps) => {
console.log(props.options); // =>
// [
// { name: 'firstName', value: 'firstName', label: 'First Name' },
// { name: 'lastName', value: 'lastName', label: 'Last Name' }],
// ]
return <ValueSelector {...props} />;
};

const App = () => (
<QueryBuilder fields={fields} controlElements={{ fieldSelector: MyFieldSelector }} />
);

If name and value differ for a given option, value takes precedence.

Workarounds

There are several ways to handle this ambiguity. One way is to cast option list props to Option[] with the as keyword. If your option list is definitely Option[], this will probably work without causing issues. However, while the as keyword may help avoid TypeScript errors, it has no effect on execution. Therefore it can be akin to lying to the TypeScript compiler (preventing it from doing its job), so this method is not recommended.

const MyComponent(props: ValueSelectorProps) => {
return <div>{(props.options as Option[]).map(opt => opt.name).join(', ')}</div>;
// ^^^^^^^^^^^ avoids TypeScript error; may have issues during execution
};
info

All default option lists (defaultCombinators, defaultOperators, etc.) are type Option[].

Another solution is to use the isOptionGroupArray type guard to determine if the option list is OptionGroup[]. This not only avoids the "lying" issue of the as keyword, but also allows you to perform separate actions depending on whether the option list type is Option[] or OptionGroup[].

const MyComponent(props: ValueSelectorProps) => {
if (isOptionGroupArray(props.options)) {
return <div>{props.options.flatMap(og => og.options).map(opt => opt.name).join(', ')}</div>;
}
return <div>{(props.options).map(opt => opt.name).join(', ')}</div>;
};

Utilities

Several utility functions are available to help manage the dual-potential nature of option list props without the need for type guards or casting with as.

getOption

function getOption(arr: OptionList, identifier: string): Option;

Retrieves the full option object from an option list based on the given identifier (corresponding to name or value), regardless of whether the list is Option[] or OptionGroup[].

Examples

getOption(
[
{ name: 'firstName', label: 'First Name' },
{ name: 'lastName', label: 'Last Name' },
],
'lastName'
);
// => { name: 'lastName', label: 'Last Name' }

getOption(
[
{ label: 'First', options: [{ name: 'firstName', label: 'First Name' }] },
{ label: 'Last', options: [{ name: 'lastName', label: 'Last Name' }] },
],
'lastName'
);
// => { name: 'lastName', label: 'Last Name' }

getFirstOption

function getFirstOption(arr: OptionList): Option;

Retrieves the value of the identifier property (name or value) of the first Option in the list, regardless of whether the list is Option[] or OptionGroup[].

QueryBuilder uses this function to set default values for option lists in new rules and groups when no other method for determining the default value is provided.

Examples

getFirstOption([
{ name: 'firstName', label: 'First Name' },
{ name: 'lastName', label: 'Last Name' },
]);
// => 'firstName'

getFirstOption([
{ label: 'First', options: [{ name: 'firstName', label: 'First Name' }] },
{ label: 'Last', options: [{ name: 'lastName', label: 'Last Name' }] },
]);
// => 'firstName'

toOptions

function toOptions(arr: OptionList): ReactElement;

Generates an array of <option> elements if the provided option list is an Option array, or an array of <optgroup> elements if the list is an OptionGroup array. Intended for use as the children prop on a <select> element, as ValueSelector does.

info

Some of the compatibility packages implement their own toOptions method that generates "option" elements appropriate for their respective style library.

Usage

const MyComponent(props: ValueSelectorProps) => {
return (
<select value={props.value} onChange={e => props.handleOnChange(e.target.value)}>
{toOptions(props.options)}
</select>
)
}
Examples

Option[] example

toOptions([
{ value: 'firstName', label: 'First Name' },
{ value: 'lastName', label: 'Last Name' },
]);
// yields (approximately):
[
<option key={'firstName'} value={'firstName'}>
First Name
</option>,
<option key={'lastName'} value={'lastName'}>
Last Name
</option>,
];

OptionGroup[] example

toOptions([
{ label: 'First', options: [{ value: 'firstName', label: 'First Name' }] },
{ label: 'Last', options: [{ value: 'lastName', label: 'Last Name' }] },
]);
// yields (approximately):
[
<optgroup key={'First'} label={'First'}>
<option key={'firstName'} value={'firstName'}>
First Name
</option>
</optgroup>,
<optgroup key={'Last'} label={'Last'}>
<option key={'lastName'} value={'lastName'}>
Last Name
</option>
</optgroup>,
];

toFlatOptionArray

function toFlatOptionArray(arr: any): boolean;

Flattens OptionGroup arrays into Option arrays using Array.prototype.flatMap, in case OptionGroup[] is inappropriate or inapplicable, and leaves Option arrays unmodified. The flattened list is de-duplicated using uniqByIdentifier.

isOptionGroupArray

function isOptionGroupArray(arr: any): boolean;

A type guard to distiguish between the two different array types represented by OptionList. If the result is true, the array in question is an OptionGroup[], so the options themselves will be within an options array property on each option group.

Examples
isOptionGroupArray([
{ value: 'firstName', label: 'First Name' },
{ value: 'lastName', label: 'Last Name' },
]);
// => false

isOptionGroupArray([
{ label: 'First', options: [{ value: 'firstName', label: 'First Name' }] },
{ label: 'Last', options: [{ value: 'lastName', label: 'Last Name' }] },
]);
// => true

uniqOptList

function uniqOptList(arr: any): boolean;

Removes duplicate options from an OptionList based on a matching identifier properties (name or value), regardless of whether the list is Option[] or OptionGroup[].

uniqByIdentifier

function uniqByIdentifier(arr: any): boolean;

Removes duplicate options from an Option array based on a matching identifier properties (name or value).