[React Typescript] Discriminated unions in components props
import React, { useState } from "react";
type Base = { id: string } | string;
type GenericSelectProps<TValue> = {
formatLabel: (value: TValue) => string;
values: Readonly<TValue[]>;
};
interface SingleSelectProps<TValue> extends GenericSelectProps<TValue> {
isMulti: false;
onChange: (value: TValue) => void;
}
interface MultiSelectProps<TValue> extends GenericSelectProps<TValue> {
isMulti: true;
onChange: (value: TValue[]) => void;
}
type T = { id: string };
const isStringValue = <TValue extends T>(
value: TValue | string
): value is string => {
return typeof value === "string";
};
const getStringFromValue = <TValue extends Base>(value: TValue) => {
if (isStringValue(value)) return value;
return value.id;
};
export const GenericSelect = <TValue extends Base>(
props: SingleSelectProps<TValue> | MultiSelectProps<TValue>
) => {
const { values, formatLabel } = props;
const [selectedValues, setSelectedValues] = useState<TValue[]>([]);
const onSelectChange = (e) => {
const val = values.find(
(value) => getStringFromValue(value) === e.target.value
);
if (!val) return;
if (props.isMulti) {
const newValues = [...selectedValues, val];
props.onChange(newValues);
setSelectedValues(newValues);
} else {
if (val) props.onChange(val);
}
};
const onDeleteValue = (index: number) => {
return () => {
const newValues = selectedValues.filter((v, i) => i !== index);
if (props.isMulti) {
props.onChange(newValues);
setSelectedValues(newValues);
}
};
};
const visibleValues = !props.isMulti
? values
: values.filter((v) => !selectedValues?.includes(v));
return (
<>
{props.isMulti ? (
<>
{selectedValues.map((v, index) => (
<span onClick={onDeleteValue(index)}>{formatLabel(v)} [x]</span>
))}
<br />
</>
) : (
""
)}
<select onChange={onSelectChange} multiple={props.isMulti}>
{visibleValues.map((value) => (
<option
key={getStringFromValue(value)}
value={getStringFromValue(value)}
>
{formatLabel(value)}
</option>
))}
</select>
</>
);
};
Usage:
const select = (
<GenericSelect<Book>
// I can't log "value.title" here, typescript will fail
// property "title" doesn't exist on type "Book[]""
// even if I know for sure that this is a single select
// and the type will always be just "Book"
onChange={(value) => console.info(value.title)}
isMulti={false}
...
/>
);
const multiSelect = (
<GenericSelect<Book>
// I can't iterate on the value here, typescript will fail
// property "map" doesn't exist on type "Book"
// even if I know for sure that this is a multi select
// and the type will always be "Book[]"
onChange={(value) => value.map(v => console.info(v))}
isMulti={true}
...
/>
);
Refer: https://www.developerway.com/posts/advanced-typescript-for-react-developers-discriminated-unions