So, I tried to create an autocomplete that can accept both options that are of type string[]
and options of type {title: string, year: number}[]
. Something like this:
const top5Films = [
{ title: "The Shawshank Redemption", year: 1994 },
{ title: "The Godfather", year: 1972 },
{ title: "The Godfather: Part II", year: 1974 },
{ title: "The Dark Knight", year: 2008 },
{ title: "12 Angry Men", year: 1957 }
];
export default function FreeSolo() {
return (
<Stack spacing={2} direction={"row"} sx={{ width: 600 }}>
<UnionAutocomplete options={top5Films} /> // {title: string, year: number}[]
<UnionAutocomplete options={top5Films.map(({ title }) => title)} /> // string[]
</Stack>
);
}
I wrote a component that does what I intended, however although the example is working, I get the following type error.
export function UnionAutocomplete(props: {
options: string[] | { title: string; year: number }[];
}) {
return (
<Autocomplete
multiple
fullWidth
// error on options
//Type '{ title: string; year: number; }' is not assignable to type 'string'
options={props.options}
getOptionLabel={
// title here is never cause option considered only as a string
(option) => (typeof option === "string" ? option : option.title)
}
renderInput={(params) => <TextField {...params} />}
/>
);
}
So my question is: why exactly can't I pass an array of either type string[]
or type {title: string, year: number}[]
to Autocomplete options. I find it difficult to understand what is stopping me from doing this.
I understand that I can just make two different components for different option types, but is there any more concise solution? Perhaps I somehow incorrectly specified the type itself.
The working code (despite the type errors) I wrote above can be found in this CodeSandbox.
The types for Autocomplete
are structured in a manner where it tries to resolve the type of a single option (represented as T
in the types) and then expects the options
prop to receive an array of those.
The way you defined the type of your options is not compatible with that since you have two different "option" types. But with a small tweak, you can make it work. What you want is:
options: (string | { title: string; year: number })[];
This allows MUI's types to determine that your individual options have a single type of (string | { title: string; year: number })
and then the options
prop is an array of those.
Here's a modified version of your sandbox demonstrating this approach:
https://codesandbox.io/s/autocomplete-option-types-7ipfq4?file=/demo.tsx
If (as indicated in comments) you want to constrain this further to prevent an array that mixes the two types, you can use the following:
interface TitleYear {
title: string;
year: number;
}
export function UnionAutocomplete<
ArrayType extends string[] | TitleYear[],
OptionType extends string | TitleYear
>(props: { options: ArrayType }) {
return (
<Autocomplete
multiple
fullWidth
options={props.options as OptionType[]}
getOptionLabel={
(option) => (typeof option === "string" ? option : option.title) // why never ?
}
renderInput={(params) => <TextField {...params} />}
/>
);
}
In the above example, OptionType[]
is equivalent to my previous solution, but ArrayType
is equivalent to what you had in your question. The net effect is that the input to UnionAutocomplete
is constrained as desired and then the internal cast to OptionType[]
allows MUI's types to resolve successfully.