兄弟函数的通用参数的打字稿推断

Typescript inference of sibling function's generic arguments

我想实现如下推理:

type RequestAction<Data = any, TransformedData = Data> = {
  type: string;
  request: any | any[];
  meta?: {
    getData?: (data: Data, currentData: TransformedData) => TransformedData;
    [extraProperty: string]: any;
  };
};

function fetchBooks(
  x: number,
  y: string,
): RequestAction<{ raw: boolean }, { parsed: boolean }> {
  return {
    type: 'FETCH_BOOKS',
    request: {
      url: '/books',
      x,
      y,
    },
    meta: {
      getData: data => ({ parsed: data.raw }),
    },
  };
}

const query = useQuery({
  action: fetchBooks,
  variables: [1, '2'], // inferred from fetchBooks argument types
});
query.data // inferred from `fetchBooks` generic

我只能同时实现一个,而不能同时实现两个。这是我的尝试:

1)

interface QueryState<QueryStateData> {
  data: QueryStateData;
  error: any;
  loading: boolean;
}

type Arr = readonly any[];

export function useQuery<
  QueryStateData = any,
  Variables extends Arr = any
>(props: {
  action?: (...args: Variables) => RequestAction<any, QueryStateData>;
  variables?: Variables;
}): QueryState<QueryStateData>;

这几乎可以工作,data 是根据传递给 fetchBooks: RequestAction 的 QueryStateData 自动推断出来的。变量也部分起作用,但只是部分起作用。它似乎工作正常,例如,如果 fetchBooks 参数是 (string, number),那么 variables 的类型确实是 [string, number] 元组。但是出于某种原因,如果我设置 variables: [1, '1'],那么它仍然可以正常工作,因为 TS 突然输入 (string | number)[],将它转换为联合数组,出于我不知道的原因,这是不可接受的,我想要变量精确 action 个参数,顺序敏感。

2)

interface RequestCreator<QueryStateData> {
  (...args: any[]): RequestAction<any, QueryStateData>;
}

export function useQuery<
  QueryStateData = any,
  R extends RequestCreator<QueryStateData> = RequestCreator<QueryStateData>
>(props: {
  action?: R;
  variables?: Parameters<R>;
}): QueryState<QueryStateData>;

有了这个,我可以实现完美的 variables 推理。不幸的是 query.data 将是 any 的类型,QueryStateData 推理在这种情况下由于某种原因不起作用,它不是从 action 回调泛型中选取的。

请告诉我哪种方法更正确,以及如何调整它以实现 variablesdata 推理。或者也许还有另一种我没有考虑的方法。提前致谢!

尝试对操作使用单一类型参数,这与您的第二次尝试类似。

interface RequestCreator<QueryStateData = any> {
  (...args: any[]): RequestAction<any, QueryStateData>;
}

// Gets the `QueryStateData` type from a `RequestCreator`
// (e.g. GetQueryStateData<typeof fetchBooks> is { parsed: boolean }).
type GetQueryStateData<T extends RequestCreator> = T extends RequestCreator<infer QueryStateData>
  ? QueryStateData
  : never;

// I'm just using declare here so TS is happy
export declare function useQuery<R extends RequestCreator>(props: {
  action?: R;
  // The parameter types of R
  // (e.g. Parameters<typeof fetchBooks> is [x: number, y: string])
  variables?: Parameters<R>;
}): QueryState<GetQueryStateData<R>>;

const query = useQuery({
  action: fetchBooks,
  variables: [1, '2'],
});
// inferred correctly as { parsed: boolean }
query.data;

useQuery({
  action: fetchBooks,
  // Type 'number' is not assignable to type 'string'. (expected)
  variables: [1, 1],
});

我认为(根据轶事经验;请不要在这方面引用我的话)当类型参数直接对应于参数类型(或直接“包含”在其中的类型,例如 action).这就是编译器在您第二次尝试时无法推断 QueryStateData 类型的原因。

Playground link

这是最终类型,我稍微更新了 cherryblossom 响应以仍然允许将自定义泛型传递给 useQuery:

interface RequestCreator<QueryStateData = any> {
  (...args: any[]): RequestAction<any, QueryStateData>;
}

type GetQueryStateData<T extends RequestCreator> = T extends RequestCreator<
  infer QueryStateData
>
  ? QueryStateData
  : never;

export function useQuery<
  Data = undefined,
  QueryCreator extends RequestCreator = any
>(props: {
  type: string | QueryCreator;
  action?: QueryCreator;
  requestKey?: string;
  multiple?: boolean;
  defaultData?: any;
  dispatch?: boolean;
  variables?: Parameters<QueryCreator>;
}): QueryState<
  Data extends undefined ? GetQueryStateData<QueryCreator> : Data
> & {
  load: () => Promise<{
    data?: QueryState<
      Data extends undefined ? GetQueryStateData<QueryCreator> : Data
    >;
    error?: null;
    isAborted?: true;
    action: any;
  }>;
};