I am trying to define reusable Autocomplete component with MUI Autocomplete as a foundation.
I want to define my own onChange callback (for a property) that accepts:
- string if DisableClearable is true
- string | null if DisableClearable is false
Each predefined option is object with 2 fields:
{ label: string, value: string }
So I assume that onChange value maybe be Value | string | null
- Value – object is label and value
- string – if freeSolo
- null – if DisableClearable false
Here is my onChange:
onChange={(_, value: Value | string | null) => {
onChange((typeof value === 'string' ? value : value?.value) || null);
}}
That worked perfectly while DisableClearable was always false
But now I define DisableClearable generic
export type AutocompleteProps<
Value extends LabelValuePair,
FreeSolo extends boolean,
DisableClearable extends boolean,
> = Omit<
MuiAutocompleteProps<Value, false, DisableClearable, FreeSolo>,
| 'renderInput'
| 'onChange'
| 'multiple'
| 'autoSelect'
| 'selectOnFocus'
| 'clearOnBlur'
| 'handleHomeEndKeys'
> & {
error?: boolean;
onChange: (value: DisableClearable extends true ? string : string | null) => void;
};
And now for
onChange={(_, value: Value | string | null) => {
onChange((typeof value === 'string' ? value : value?.value) || null);
}}
I receive TS error:
TS2345: Argument of type string | null is not assignable to parameter of type
DisableClearable extends true ? string : string | null
Type null is not assignable to type
DisableClearable extends true ? string : string | null
So I wonder how to resolve this issue and allow ts to infer type of onChange dynamically
I expect that I can pass to the component:
(value: string) => void if DisableClearable is true
(value: string | null) => void if DisableClearable is false
Here is full component:
export type AutocompleteProps<
Value extends LabelValuePair,
FreeSolo extends boolean,
DisableClearable extends boolean,
> = Omit<
MuiAutocompleteProps<Value, false, DisableClearable, FreeSolo>,
| 'renderInput'
| 'onChange'
| 'multiple'
| 'autoSelect'
| 'selectOnFocus'
| 'clearOnBlur'
| 'handleHomeEndKeys'
> & {
error?: boolean;
onChange: (
value: DisableClearable extends true ? string : string | null,
) => void;
};
export const Autocomplete = <
Value extends LabelValuePair,
FreeSolo extends boolean,
DisableClearable extends boolean,
>({
error,
onChange,
freeSolo,
value,
...props
}: AutocompleteProps<Value, FreeSolo, DisableClearable>) => {
const freeSoloParams = freeSolo
? {
selectOnFocus: true,
clearOnBlur: true,
handleHomeEndKeys: true,
}
: {};
// we are trying to find selected option in options otherwise using value (for example when free solo)
const selectedValue = useMemo(
() => props.options.find((option) => option.value === value) || value,
[props.options, value],
);
return (
<MuiAutocomplete
{...props}
{...freeSoloParams}
value={selectedValue}
filterOptions={(options, params) => {
const filter = createFilterOptions<Value>();
const filtered = filter(options, params);
if (!freeSolo) {
return filtered;
}
const { inputValue } = params;
const isExisting = options.some(
(option) => inputValue === option.label,
);
if (inputValue && !isExisting) {
filtered.push({
value: inputValue,
label: `Add "${inputValue}"`,
} as Value);
}
return filtered;
}}
freeSolo={freeSolo}
onChange={(_, value: Value | string | null) => {
onChange((typeof value === 'string' ? value : value?.value) || null);
}}
renderInput={(params) => <Input {...params} error={error} />}
/>
);
};