I have a component named ResourceSelect, it's basically a wrapper over a normal Select component, but uses resources(dynamic format like {id, name, title}) instead of options(fixed format with {label, value}) to render the select items. It worked in JavaScript environment, but I found myself cannot implement a good typescript definition for it. Currently I can do is using function overload and generic params to achieve directly usage like this:
export type ValueableType = number | string;
export type DefaultResourceType = Record<string, string | number>;
export interface BaseResourceSelectProps<
RT extends DefaultResourceType = DefaultResourceType,
VT extends ValueableType | number[] | string[] = number
> {
/** resource list used to render options */
resources?: RT[];
/** the default key to render label */
labelKey?: keyof RT;
// In my real implementation it's from extends AntdSelectProps<VT>
// But I think value and onChange can represent this part
value?: VT;
onChange?: (value: VT) => void;
}
export interface GenericResourceSelectProps extends BaseResourceSelectProps {
mode?: 'multiple';
valueKey?: string;
}
export function ResourceSelect<
RT extends DefaultResourceType = DefaultResourceType,
VK extends keyof RT = 'id'
>(
props: {
valueKey?: VK;
mode?: never;
} & BaseResourceSelectProps<RT, RT[VK]>
): JSX.Element;
export function ResourceSelect<
RT extends DefaultResourceType = DefaultResourceType,
VK extends keyof RT = 'id'
>(
props: {
valueKey?: VK;
mode: 'multiple';
} & BaseResourceSelectProps<RT, RT[VK] extends string ? string[] : number[]>
): JSX.Element;
/**
* ResourceSelect
*/
export function ResourceSelect(genericProps: unknown) {
const props = genericProps as
// some implementation
}
// Storybook cases
export const CodeBoard: Story<{
value: number | string | number[] | string[];
onSelect: (v: unknown) => void;
}> = ({ value, onSelect }) => {
return (
<div style={{ maxWidth: 200 }}>
<h3>Normal usage</h3>
<ResourceSelect
resources={resources}
value={value as number}
defaultValue={1}
onChange={(num: number) => onSelect(num)}
style={{ width: '100%' }}
/>
<h3>Custome valueKey</h3>
<ResourceSelect
resources={resources}
value={value as string}
valueKey="strId"
defaultValue="id 1"
onChange={(str: string) => onSelect(str)}
style={{ width: '100%' }}
/>
<h3>Multiple mode</h3>
<ResourceSelect
resources={resources}
value={value as number[]}
defaultValue={[1]}
mode="multiple"
onChange={(numArr: number[]) => onSelect(numArr)}
style={{ width: '100%' }}
/>
</div>
);
};
DesignBoard.args = {
value: undefined,
};
DesignBoard.argTypes = { onSelect: { action: 'onSelect' } };
My problem now is that I cannot re-encapsulate this component. Code like this just not working:
type ResourceSelectProps = Parameters<typeof ResourceSelect>[0];
interface Location {
id: number;
address: string;
}
/**
* LocationSelect
*/
export const LocationSelect: FC<ResourceSelectProps<Location>> = (props) => {
// Call an API hook that fetches locations
const { data = [], isValidating } = useLocations();
return (
<ResourceSelect
resources={data}
placeholder="Location"
loading={isValidating}
labelKey="address"
dropdownMatchSelectWidth={false}
{...props}
/>
);
};
