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—such as fields
, combinators
, and operators
—use the OptionList
type. OptionList
is a union type supporting two array formats: Option[]
and OptionGroup[]
. While this design provides flexibility, it introduces complexity when consuming option list props in custom subcomponents.
This guide offers strategies for managing the inherent ambiguity in option list TypeScript types.
Option
objects havelabel
,name
, andvalue
properties, all of which extendstring
.OptionGroup
objects have alabel
property along with anoptions
property which is an array ofOption
objects.
This mirrors the children
prop structure of <select>
elements, which can contain either <option>
elements directly or <optgroup>
elements with nested <option>
lists.
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 typically occurs when treating option list elements as guaranteed Option
types. For example, mapping through the list to access the name
property:
const ListAllOptionNames = (props: ValueSelectorProps) => {
return <div>{props.options.map(opt => opt.name).join(', ')}</div>;
// ^^^^ error
};
While it seems logical that an option list passed to QueryBuilder
as Option[]
would remain Option[]
in subcomponents, React Query Builder's type generics only infer the option types within the list, not the list structure itself.
All options in subcomponent props are guaranteed to include both name
and value
properties, even if the original QueryBuilder
prop omitted one of them.
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
Several approaches can handle this ambiguity. One option is casting option list props to Option[]
using the as
keyword. While this may work if your option list is definitely Option[]
, the as
keyword only suppresses TypeScript errors without affecting runtime behavior. This essentially misleads the TypeScript compiler and prevents proper type checking, making it an unrecommended approach.
const MyComponent(props: ValueSelectorProps) => {
return <div>{(props.options as Option[]).map(opt => opt.name).join(', ')}</div>;
// ^^^^^^^^^^^ avoids TypeScript error; may have issues during execution
};
All default option lists (defaultCombinators
, defaultOperators
, etc.) are type Option[]
.
A better solution uses the isOptionGroupArray
type guard to determine the option list type. This approach avoids the deception of type casting while enabling different behaviors for Option[]
versus OptionGroup[]
arrays.
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 simplify working with option list props without requiring type guards or as
casting.
getOption
function getOption(arr: OptionList, identifier: string): Option;
Retrieves the complete option object from an option list using the given identifier (name
or value
), working with both Option[]
and OptionGroup[]
formats.
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;
Returns the identifier value (name
or value
) of the first Option
in the list, supporting both Option[]
and OptionGroup[]
formats.
QueryBuilder
uses this function to establish default values for option lists in new rules and groups when no other default determination method is available.
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;
Creates <option>
elements for Option
arrays or <optgroup>
elements for OptionGroup
arrays. Designed for use as the children
prop of <select>
elements, as implemented in ValueSelector
.
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;
Converts OptionGroup
arrays to flattened Option
arrays using Array.prototype.flatMap
when grouped structures are unsuitable, while leaving Option
arrays unchanged. The result is deduplicated using uniqByIdentifier
.
isOptionGroupArray
function isOptionGroupArray(arr: any): boolean;
A type guard that distinguishes between the two array types in OptionList
. When this returns true
, the array is OptionGroup[]
, meaning actual options are nested within each group's options
property.
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
by comparing identifier properties (name
or value
), working with both Option[]
and OptionGroup[]
formats.
uniqByIdentifier
function uniqByIdentifier(arr: any): boolean;
Removes duplicate options from an Option
array by comparing identifier properties (name
or value
).