Problem Description:
I have a custom React hook useFetchSelectOptions
that fetches data and formats it into selectable options. Depending on the returnForm
parameter, the options need to be either of type SelectOption[] | null
or `Record<string, string> | null, but never a mix of both.
I want to ensure that the return type of options
inside the hook strictly matches the specified returnForm
. For instance:
returnForm
is 'arr'
, options should be of type SelectOption[] | null
.returnForm
is 'dict'
, options should be of type Record<string, string> | null
.Currently, the options
type could potentially be Record<string, string> | SelectOption[] | null
due to the conditional formatting in the hook, which can lead to ambiguity for the consumer of this hook.
Goal:
I would like to implement an assertion mechanism inside the hook to ensure that options
adheres strictly to the determined type based on the returnForm
parameter. This way, consumers using the hook can confidently rely on the inferred type without additional type checks.
Code Sample:
import { useState, useEffect } from 'react';
export interface SelectOption {
label: string;
value: string;
}
// Define a type for the return form
type ReturnForm = 'dict' | 'arr';
export function useFetchSelectOptions<T extends { name: string; id: string }>(
fetchData: () => Promise<T[]>,
returnForm: ReturnForm = 'arr', // Default to array format if not specified
): [T[] | null, SelectOption[] | Record<string, string> | null] {
const [data, setData] = useState<T[] | null>(null);
useEffect(() => {
const fetchDataAndMapToOptions = async () => {
try {
const fetchedData = await fetchData();
setData(fetchedData);
} catch (error) {
console.error('Error fetching data:', error);
setData(null);
}
};
fetchDataAndMapToOptions();
}, [fetchData]);
let options: SelectOption[] | Record<string, string> | null = data
? data.map((item) => ({ label: item.name, value: item.id }))
: null;
// Assertion to strictly type 'options' based on 'returnForm'
if (returnForm === 'dict') {
options = options?.reduce(
(acc, curr) => {
if (curr) {
acc[curr.value] = curr.label;
}
return acc;
},
{} as Record<string, string>,
) ?? null;
}
return [data, options];
}
Additional Notes:
I've added a basic type assertion within the useFetchSelectOptions
hook to refine the type of options
based on the returnForm
parameter. This ensures that options
will conform to either SelectOption[] | null
or Record<string, string> | null
as expected, depending on the specified returnForm
.
I'm seeking feedback on this approach and any potential improvements or alternative strategies to achieve strict typing of options
within the context of the hook.
Thank you for your assistance!
consumers using the hook can confidently rely on the inferred type without additional type checks.
Because the return type of the hook is already typed as [T[] | null, SelectOption[] | Record<string, string> | null]
, that is all that callers will see. Which is not what you are looking for, IIUC?
assertion mechanism inside the hook
Whatever is done inside the function does not affect the "outside", since the return type is already defined, as said above. TS will only check that the actual value that is returned satisfies said return type.
If you want callers to see the return type corresponding to the returnForm
value, then you should rather work on the Hook function signature. In your case, a function overload is probably the most appropriate:
export function useFetchSelectOptions<T extends { name: string; id: string }>(fetchData: () => Promise<T[]>, returnForm?: "arr"): [T[], SelectOption[] | null]
export function useFetchSelectOptions<T extends { name: string; id: string }>(fetchData: () => Promise<T[]>, returnForm: "dict"): [T[], Record<string, string> | null]
export function useFetchSelectOptions<T extends { name: string; id: string }>(
fetchData: () => Promise<T[]>,
returnForm: ReturnForm = 'arr', // Default to array format if not specified
): [T[] | null, SelectOption[] | Record<string, string> | null] {
// Implementation (untouched)
}
const arr = useFetchSelectOptions(async () => []);
// ^? [never[], SelectOption[] | null]
const dict = useFetchSelectOptions(async () => [], "dict");
// ^? [never[], Record<string, string> | null]