5

我有 awesome 的完全类型安全的自动生成代码graphql-codgen/vue。我通过构建一个小包装器在我的项目中使用它,这样我的用户就不必在每次调用时都执行常见的配置任务。例如定义缓存行为、自动更新缓存、解构导致正确的类型和格式。

使用 JS 和 with 的包装器工作者,any但我也希望它是类型安全的,并且由于graphql-codegen已经以类型安全的方式生成所有类型和方法,我认为必须有一种方法可以做到这一点。不知何故,我认为有歧视性的工会......

所以归结为示例代码我的问题是:我有这个自动生成的代码:

//File GQLService.ts
export type CustodiansList = (
  { __typename: 'Query' }
  & { custodiansList?: Maybe<Array<(
    { __typename: 'Custodian' }
    & Pick<Custodian, 'id' | 'name' | 'street' | 'zip' | 'city' | 'telephone' | 'createdAt' | 'updatedAt'>
  )>> }
);

type ReactiveFunctionCustodiansList = () => CustodiansListVariables

/**
 * __useCustodiansList__
 *
 * To run a query within a Vue component, call `useCustodiansList` and pass it any options that fit your needs.
 * When your component renders, `useCustodiansList` returns an object from Apollo Client that contains result, loading and error properties
 * you can use to render your UI.
 *
 * @param baseOptions options that will be passed into the query, supported options are listed on: https://v4.apollo.vuejs.org/guide-composable/query.html#options;
 *
 * @example
 * const { result, loading, error } = useCustodiansList(
 *   {
 *   }
 * );
 */
export function useCustodiansList(variables?: CustodiansListVariables | VueCompositionApi.Ref<CustodiansListVariables> | ReactiveFunctionCustodiansList, baseOptions?: VueApolloComposable.UseQueryOptions<CustodiansList, CustodiansListVariables>) {
          return VueApolloComposable.useQuery<CustodiansList, CustodiansListVariables>(CustodiansListDocument, variables, baseOptions);
        }

export type CustodiansListCompositionFunctionResult = ReturnType<typeof useCustodiansList>;

现在我想用最少的 DRY 像这样“动态地”使用它:

import * as Service from "./GQLService"; // from above
// e.g. typename = "custodian"
function useQueryList(typename:string) {
 const fnName = toFunctionName(typename) // e.g. useCustodiansList
 const result = Service[fnName](); //! this is the problem

 // we also want to return everything including a parsedResult 
 const listName = `${typename}sList`
 return {
    [listName]: parseResult(result),
    ...result
  }
}

意图

我真的不想重新创建graphql-codgen所做的所有工作TypeTable,因为我认为所有这些工作都已经由graphql-codegen完成。

我的目标是有人可以创建一个新的ExamplesList.graphqlgraphql-codegen包装它,然后准备好使用useQueryList("example")

因此,虽然这是一个动态传递的参数,但它也必须能够通过某种方式映射所有服务函数的返回类型,然后得到返回的一个,Array<__typename>或者我错了吗?而且我认为我必须以某种方式必须typename通过解析__typenames来自Service

const result = Service[fnName](); //! this is the problem

实际上并不是我们所做的一切,我们对其进行了更多的包装和转换,但是一旦我在这里获得了正确的类型,一切都会好起来的。

4

2 回答 2

3

我认为这个问题与 TypeScript 的关系比与 GraphQL Codegen 的关系更大。基本上,您要做的是动态地从对象中获取函数属性,我不确定在不向 codegen 输出添加内容的情况下使用 TypeScript 是否可行。

您可以创建一个自定义代码生成插件,该插件将根据您的所有查询生成一个对象,并使用您希望拥有的单数键(或者,也许只是操作名称)。这样,您将能够获得 和 之间的"example"映射useExamplesListQuery

于 2020-05-17T07:12:48.693 回答
3

我对您的设置进行了一些尝试,因为我发现它非常有趣!

在这种情况下,您需要做一些 TypeScript 取证 :) 在映射类型的帮助下,我能够组合出以下解决方案。我不知道你的解析函数做了什么,所以我让它返回unknown,但这应该是一件容易解决的事情。

// Basic shape of a query result with __typename.
//
// I know your example only worked with lists,
// I added the singular form just in case :)
type QueryResultWithTypeName<T> = { __typename: T } | Array<{ __typename: T }>;

// A __typename (Custodian etc) based on a query result (CustodiansList etc)
type TypeNameForResult<R> = NonNullable<
  {
    [K in keyof R]: NonNullable<R[K]> extends QueryResultWithTypeName<infer T> ? T : never;
  }[keyof R]
>;

// A result property name (custodiansList etc) based on a query result object (CustodiansList etc)
type PropertyNameForResult<R> = NonNullable<
  {
    [K in keyof R]: NonNullable<R[K]> extends QueryResultWithTypeName<string> ? K : never;
  }[keyof R]
>;

// List of all available type names (Custodian etc)
type TypeName = {
  [K in keyof ServiceType]: ServiceType[K] extends () => UseQueryReturn<infer TResult, any>
    ? TypeNameForResult<TResult>
    : never;
}[keyof ServiceType];

// Map of type names (Custodian etc) and functions (useCustodianList etc)
//
// e.g. type UseCustodiansList = FunctionByTypeName['Custodian']
type FunctionByTypeName = {
  [K in TypeName]: {
    [L in keyof ServiceType]: ServiceType[L] extends () => UseQueryReturn<infer TResult, any>
      ? TypeNameForResult<TResult> extends K
        ? ServiceType[L]
        : never
      : never;
  }[keyof ServiceType];
};

// Map of type names (Custodian) and property names (custodiansList etc)
//
// e.g. type CustodianProperty = PropertyNameByTypeName['Custodian'] // will be 'custodiansList'
type PropertyNameByTypeName = {
  [K in keyof FunctionByTypeName]: FunctionByTypeName[K] extends () => UseQueryReturn<infer TResult, any>
    ? PropertyNameForResult<TResult>
    : never;
};

// Map of type names (Custodian) and function return types
//
// e.g. type CustodianProperty = ReturnTypeByTypeName['Custodian'] // will be UseQueryReturn<CustodiansList, CustodiansListVariables>
type ReturnTypeByTypeName = {
  [K in keyof FunctionByTypeName]: ReturnType<FunctionByTypeName[K]>;
};

// Type for the the return object from useQueryList
// (I was not sure what the result of your parsing is so I just used unknown)
//
// e.g. type UseCustodiansQueryReturnType = UseQueryListReturnType<'Custodian'> // will be { custodiansList: {}, /* the rest of UseQueryReturn */ }
type UseQueryListReturnType<T extends TypeName> = ReturnTypeByTypeName[T] &
  {
    [K in PropertyNameByTypeName[T]]: unknown;

    // I would suggest though to not name the parsed result depending on the type name
    // and make it consistent for all the types, e.g. call it parsedResult:
    //
    // parsedResult: unknown;
  };

// A helper function to turn 'Custodian' into 'custodian' etc to get the property name from type name later
const lowercaseFirstLetter = (value: string) => (value ? value[0].toLowerCase() + value.slice(1) : value);

// This was undefined in your example
const parseResult = <T>(a: T): T => a;

// Convert typename to a function
const toFunction = <T extends TypeName>(typename: T): FunctionByTypeName[T] => {
  // This is the first type casting you need to make since string manipulation and types don't go together
  return Service[`use${typename}sList` as keyof ServiceType];
};

// Convert typename to property name (e.g. 'Custodian' => 'custodiansList')
const toPropertyName = <T extends TypeName>(typename: T): PropertyNameByTypeName[T] =>
  // Again the same string manipulation problem
  `${lowercaseFirstLetter(typename)}sList` as PropertyNameByTypeName[T];

function useQueryList<T extends TypeName>(typename: T): UseQueryListReturnType<T> {
  const fn: FunctionByTypeName[T] = toFunction(typename); // e.g. useCustodiansList
  const result: ReturnTypeByTypeName[T] = fn(); //! this is the problem

  // we also want to return everything including a parsedResult
  const listName: PropertyNameByTypeName[T] = toPropertyName(typename);

  // Now the third type casting is something I am not proud of but unfortunately
  // TypeScript does not want to agree with me that listName is not just a string
  // but a very special string :)
  return {
    ...result,
    [listName]: parseResult(result),
  } as UseQueryListReturnType<T>;
}

当我现在尝试:

const custodians = useQueryList('Custodian');

我可以看到usersList财产在那里!耶!

于 2020-05-26T10:49:25.973 回答