自动生成函数的类型安全包装,然后仅使用 `__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.graphql
,graphql-codegen
包装它,然后useQueryList("example")
可以使用它
因此,尽管这是一个动态传递的参数,但也必须能够通过某种方式映射所有服务函数的返回类型,然后获取返回 Array<__typename>
的静态类型,或者我错了吗?而且我认为我必须以某种方式必须通过从Service
解析所有可能的__typenames
来将typename
参数从字符串归结为字符串文字
const result = Service[fnName](); //! this is the problem
实际上并不是我们所做的一切,我们对其进行了更多的包装和转换,但是一旦我在这里得到正确的类型,一切都会好起来的。
【问题讨论】:
【参考方案1】:我认为这个问题与 TypeScript 的关系比与 GraphQL Codegen 的关系更大。 基本上,您要做的是动态地从对象中获取函数属性,我不确定在不向 codegen 输出添加内容的情况下使用 TypeScript 是否可行。
您可以创建一个自定义代码生成插件,该插件将根据您的所有查询生成一个对象,并使用您希望拥有的单数键(或者,也许只是操作名称)。这样您就可以得到"example"
和useExamplesListQuery
之间的映射。
【讨论】:
感谢您跟进此事。我认为它必须以某种方式成为可能,因为所有信息已经由您的项目提供静态信息。但是,是的,也许构建 graphql-codegen-plugin 会更好 这是可能的,但据我所知只有生成的代码。我认为没有办法在 TypeScript 类型系统中进行字符串操作。 我忘了我还有字符串操作,对不起!我会满足于从所有生成的__typename
s 中生成一个字符串文字,而省略更通用的文字,如@987654324@。像这样:typescript function useQueryList<T extends keyof AllAutogeneratedFunctions>(fnName:T) const result = Service[fnName](); //! this is the problem // we also want to return everything including a parsedResult return [ReturnType<Service[fnName]>["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` 作为参数动态调用。打字稿的主要内容,如果未能解决你的问题,请参考以下文章