Home > Back-end >  return different generic type via function's optional argument in typescript
return different generic type via function's optional argument in typescript

Time:09-15

Background

I want to implement my own usePromise, e.g.

  // if with filterKey(e.g `K=list`), fileNodes's type should be `FileNode`  (i.e. T[K])
  const [fileNodes, isOk] = usePromise(
    () => {
      return Promise.resolve({list:[]}) as Promise<{ list: FileNode[] }>;
    },
    { initValue: [] },
    "list",
  );

  // if with no filterKey, data's type should be `{list: FileNode[]}`  (i.e. T)
  const [data, isOk] = usePromise(
    () => {
      return Promise.resolve({list:[]}) as Promise<{ list: FileNode[] }>;
    },
    { initValue: {list:[]} }
  );

My problem

I use K extends keyof T to check filterKey and its type K

  1. If K is property of T, it shuld return T[K]
  2. If K is not property of T, it shuld return T

Here is the code which implements usePromise. I found that K extends keyof T always result in false.

How to fix my code?

import { useState, useRef, useEffect } from 'react'

interface Options<T> {
  initValue?: T
  one rror?: (r: () => any) => any
}
interface FileNode {}

export function usePromise<T, K>(
  factory: () => Promise<T>,
  options: Options<K extends keyof T ? T[K] : T> = {},
  filterKey?: K,
): [K extends keyof T ? T[K] : T, boolean] {
  type R = K extends keyof T ? T[K] : T;
  const [state, setState] = useState<R>(
    options.initValue!,
  );
  const isLoadingRef = useRef(false);
  useEffect(() => {
    factory().then((r) => {
      if (filterKey) {
        setState((r as any)[filterKey] as unknown as R);
      } else {
        setState(r as unknown as R);
      }
    }).catch((res) => {
      if (options.onError) options.onError(res);
      else throw res;
    });
  }, []);
  return [state, isLoadingRef.current] as [R, boolean];
}

Complex solution

There is a complex solution via overloaded function. Is there any simple way?

export function usePromise<T, K extends keyof T>(
  factory: () => Promise<T>,
  options: Options<T[K]>,
  filterKey: K,
): [K extends keyof T ? T[K] : T, boolean];
export function usePromise<T>(
  factory: () => Promise<T>,
  options: Options<T>,
): [T, boolean];

CodePudding user response:

Your current implementation of usePromise infers K to be string | undefined when you are expecting 'list'. You can fix this by constraining the type of K:

export function usePromise<T, K extends keyof T | undefined>

Here is a minimal reproduction in a playground.

  • Related