Handling Single & Multi-Select in TypeScript Without Type Issues

When building a select component that supports both single and multi-select options, developers often run into TypeScript issues. One common mistake is defining an onChange function like this:

onChange: (value: string | { value: string; label: string }[])

❌ The Problem

While this may seem reasonable at first, it leads to TypeScript confusion when handling the onChange function because the return type isn't clear. This often results in errors or unnecessary type checks when using the component.

✅ The Solution

To avoid this, we can clearly separate single and multi-select behavior using TypeScript's discriminated union types. This ensures better type safety and makes the component more intuitive.

Designing the Component

We define two distinct props:

  • SingleSelectProps → Handles the selection of a single value.

  • MultiSelectProps → Handles multiple selections.

Using TypeScript, we enforce that onValueChange returns the expected type based on isMultiSelect.

📌 Code Implementation



type SingleSelectProps = {
  onValueChange: (value: string) => void;
  isMultiSelect: false;
  defaultSelected?: string;
};

type MultiSelectProps = {
  onValueChange: (value: { value: string; administrativeArea: DisplayOptions }[]) => void;
  isMultiSelect: true;
  defaultSelected?: { value: string; administrativeArea: DisplayOptions }[];
};

type SelectProps = SingleSelectProps | MultiSelectProps;

export default function SelectComponent({  ...props }: SelectProps) {
  const [multiSelectedValues, setMultiSelectedValues] = useState<
    {
      value: string;
      administrativeArea: DisplayOptions;
    }[]
  >(() => {
    if (props.isMultiSelect && props.defaultSelected) {
      return (
        props.defaultSelected || []
      );
    }
    return [];
  });

  const [singleSelectedValue, setSingleSelectedValue] = useState<string | null>(() => {
    if (!props.isMultiSelect && props.defaultSelected) {
      return props.defaultSelected || null;
    }
    return null;
  });

    const handleSelect = (value: { value: string; administrativeArea: DisplayOptions }) => {
    if (!props.isMultiSelect) {
      setSingleSelectedValue(value.value);
      props.onValueChange(value);
    } else {
      setMultiSelectedValues([...multiSelectedValues, value]);
      props.onValueChange([...multiSelectedValues, value]);
    }
  };

  return <div>{/* Render UI here */}</div>;
}

Here, we define two separate prop types:

  • SingleSelectProps for single selection, where the value passed to onValueChange is a string.

  • MultiSelectProps for multiple selection, where the value passed is an array of objects { value: string; administrativeArea: DisplayOptions }[].

The SelectProps type then combines these two types using a union. This ensures the component handles both scenarios correctly.

How It Works:

  1. State Management:

    • For single selection, we store the selected value as a string.

    • For multi-selection, we store the selected values in an array of objects ({ value: string; administrativeArea: DisplayOptions }[]).

  2. Handling Selections:

    • The handleSelect function checks whether the component is in multi-select mode (props.isMultiSelect).

    • In single-select mode, we simply update the state with the selected value and call onValueChange with the string value.

    • In multi-select mode, we add the selected value to the multiSelectedValues array and call onValueChange with the updated array.

  3. Rendering:

    • The rendering part (<div>{/* Render UI here */}</div>) is where you would implement the UI elements like a select or custom dropdown to display the options and capture user input.

🤔 Why Do We Use props.isMultiSelect and props.onValueChange?

1️⃣ Checking props.isMultiSelect to Determine Selection Mode

In our component, props.isMultiSelect acts as a discriminator to differentiate between single-select and multi-select behaviors. Since we defined SelectProps as a discriminated union type, TypeScript ensures that when isMultiSelect is true, onValueChange expects an array, and when isMultiSelect is false, it expects a string.

Using props.isMultiSelect, we avoid unnecessary type checks like typeof value === "string" inside handleSelect, making the logic clearer and type-safe.

2️⃣ Using props.onValueChange to Notify the Parent Component

The onValueChange function is provided as a prop, which allows the parent component using SelectComponent to get updates whenever a user selects an option.

🔥 Why This Approach is Better:

No manual type-checking: TypeScript automatically infers the correct type based on isMultiSelect.
Clearer logic: No need for complex conditionals—each behavior is handled cleanly.
Reusable and flexible: The parent component can easily integrate this component without worrying about handling selection logic.

This design ensures that our SelectComponent is both easy to use and type-safe, preventing common pitfalls in TypeScript when handling mixed-type selections. 🚀

🚀 Key Benefits

No TypeScript confusion – The onValueChange function always returns a predictable type.
Cleaner API – The component is intuitive to use and extend.
Better Developer Experience – No unnecessary type assertions or as casting.

📢 Conclusion

By leveraging discriminated unions and clear type separation, we can build a robust and scalable select component that seamlessly handles both single and multi-select use cases. This approach ensures that TypeScript helps us prevent errors rather than introducing them!

What are your thoughts? Have you faced similar TypeScript issues? Let's discuss in the comments! 🚀

🎯 Play around with the working example on CodeSandbox:
🔗 https://codesandbox.io/p/sandbox/45sywq

Did you find this article valuable?

Support Arjun's Blog by becoming a sponsor. Any amount is appreciated!