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 toonValueChange
is astring
.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:
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 }[]
).
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 callonValueChange
with the updated array.
Rendering:
- The rendering part (
<div>{/* Render UI here */}</div>
) is where you would implement the UI elements like aselect
or custom dropdown to display the options and capture user input.
- The rendering part (
🤔 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