reactjstypescriptmaterial-uiautocompletemui-autocomplete

Why MUI Autocomplete options type can't be A[] | B[]


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.

Edit awesome-platform-40mf0g


Solution

  • 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} />}
        />
      );
    }
    

    Edit autocomplete option types

    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.