Search code examples
typescript

Infer possible undefined return if no default value provided


I have a React hook that requests a remote API and returns something. By default, while the API is being fetch, the returned value is undefined.

Still, I introduced a new prop that allows returning a default value – so, while the API is fetch, the default value is returned.

Is it possible for TypeScript to infer that when defaultValue is defined, then the return can not be undefined?

This is my code so far:

interface UseMyHookOptions<T> {
  defaultValue?: T
}

type UseMyHookReturn<T> = T extends undefined 
  ? [T | undefined, (data: T) => void] 
  : [T, (data: T) => void]

function useMyHook<T>(key: string, options: UseMyHookOptions<T> = {}): UseMyHookReturn<T>{
  const defaultValue = options?.defaultValue

  const remoteValue = useFetch<T>(`/api/${key}`)

  const setValue = (value: T) => { console.log(value) }

  const value = remoteValue ?? defaultValue;
  return [value, setValue] as UseMyHookReturn<T>;
}

Expected examples:

// `foo` type:  expected: T   (because defaultValue is defined)
//                   got: T
const [foo, setFoo] = useMyHook<string>('foo', { defaultValue: 'something' });

// `bar` type:  expected: T | undefined   (because defaultValue is not defined)
//                   got: T
const [bar, setBar] = useMyHook<string>('bar');

Playground


Solution

  • What you need here is function overloading to create multiple signatures depending on what parameters are provided.

    How about something like this?

    interface UseMyHookOptions<T> {
      defaultValue?: T
    }
    
    type UseMyHookReturn<T> = [T, (value: T) => void]
    
    function useMyHook<T>(key: string): UseMyHookReturn<T | undefined>;
    function useMyHook<T>(key: string, options: UseMyHookOptions<T> & { defaultValue: T }):  UseMyHookReturn<T>;
    function useMyHook<T>(key: string, options: UseMyHookOptions<T> & { defaultValue?: undefined }):  UseMyHookReturn<T | undefined>;
    function useMyHook<T>(key: string, options: UseMyHookOptions<T> = {}) {
      const defaultValue = options?.defaultValue
      const remoteValue = useFetch<T>(`/api/${key}`)
    
      const value = remoteValue ?? defaultValue;
      const setValue = (value: T) => { console.log(value) }
    
      return [value, setValue];
    }
    

    Here you'd be defining three overloads for your useMyHook function that changes what is returned depending on whether options (and more specifically if options.defaultValue) are provided.

    This way, you'd have:

    const [foo, setFoo] = useMyHook<string>('foo', { defaultValue: 'something' }); // foo is a string
    const [bar, setBar] = useMyHook<string>('bar'); // bar is a string | undefined
    const [baz, setBaz] = useMyHook<string>('baz', {}); // baz is a string | undefined (no defaultValue provided despite options given).
    

    You can check this playground in case you want to tinker with this a bit further.