自动生成函数的类型安全包装,然后仅使用 `__typename` 作为参数动态调用。打字稿

Posted

技术标签:

【中文标题】自动生成函数的类型安全包装,然后仅使用 `__typename` 作为参数动态调用。打字稿【英文标题】:Typesafe wrap of autogenerated functions and then call dynamically by just using `__typename` as parameter. Typescript 【发布时间】:2020-09-02 04:41:03 【问题描述】:

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

使用 JS 和 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 = `$typenamesList`
 return 
    [listName]: parseResult(result),
    ...result
  

意图

我真的不想通过创建一个有区别的联合 TypeTable 来重新创建 graphql-codgen 完成的所有工作,就像在回答的其他问题中一样,因为我认为所有这些工作都已经由 graphql 完成-代码生成器。

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

因此,尽管这是一个动态传递的参数,但也必须能够通过某种方式映射所有服务函数的返回类型,然后获取返回 Array&lt;__typename&gt; 的静态类型,或者我错了吗?而且我认为我必须以某种方式必须通过从Service 解析所有可能的__typenames 来将typename 参数从字符串归结为字符串文字

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

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

【问题讨论】:

【参考方案1】:

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

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

【讨论】:

感谢您跟进此事。我认为它必须以某种方式成为可能,因为所有信息已经由您的项目提供静态信息。但是,是的,也许构建 graphql-codegen-plugin 会更好 这是可能的,但据我所知只有生成的代码。我认为没有办法在 TypeScript 类型系统中进行字符串操作。 我忘了我还有字符串操作,对不起!我会满足于从所有生成的__typenames 中生成一个字符串文字,而省略更通用的文字,如@9​​87654324@。像这样:typescript function useQueryList&lt;T extends keyof AllAutogeneratedFunctions&gt;(fnName:T) const result = Service[fnName](); //! this is the problem // we also want to return everything including a parsedResult return [ReturnType&lt;Service[fnName]&gt;["result"]]: parseResult(result), ...result 但我想我可以直接调用 Fn。【参考方案2】:

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

在这种情况下,您需要做一些 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$typenamesList` 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 属性在那里!耶!

【讨论】:

谢谢,我还没来得及测试它,但它看起来确实不错。我将赏金奖励给你,并将其翻倍以补偿迟到的反应!

以上是关于自动生成函数的类型安全包装,然后仅使用 `__typename` 作为参数动态调用。打字稿的主要内容,如果未能解决你的问题,请参考以下文章

Mark包装类 19_12_7

tiger-complier 问题记录 类型检查

内置的包装类

STL源码剖析——Iterators与Traits编程#5 __type_traits

深入理解Block

JAVA——装箱和拆箱