如何编写类型安全的函数签名来接受 amplify-js v6 graphql 订阅通知的回调函数?

问题描述 投票:0回答:1

使用 6.0.9 版本(不是 5.x.x!)的 NPM 库

aws-amplify
,我尝试将调用包装到
client.graphql({ query: typedGqlString, variables}).subscribe({ next, error })
,以便我可以类似地处理我的所有 graphql 订阅,以便取消订阅、错误处理,并在网络故障和重新连接时刷新数据。

此 v6 版本的 amplify 库使用类似于以下内容的类型化 GQL 字符串:

import { generateClient} from "aws-amplify/api";
const client = generateClient();

type GeneratedSubscription<InputType, OutputType> = string & {
  __generatedSubscriptionInput: InputType;
  __generatedSubscriptionOutput: OutputType;
};

interface SubscriptionOnCreateClubDeviceArgs {
  clubId: string;
}

interface SubscriptionOnCreateClubDeviceCallbackPayload {
  onCreateClubDevice: ClubDevice; /* ClubDevice typescript type defined elsewhere */
}

const subscriptionOnCreateClubDevice = /* GraphQL */ `
  subscription OnCreateClubDevice($clubId: String!) {
    onCreateClubDevice(clubId: $clubId) {
      (... clubDevice gql type field names)
    }
  }
` as GeneratedSubscription<
  SubscriptionOnCreateClubDeviceArgs,
  SubscriptionOnCreateClubDeviceCallbackPayload
>

client.graphql({
  query: subscriptionOnCreateClubDevice,
  variables: { clubId: "foo" }
}).subscribe({
  next: (payload/* ": SubscriptionOnCreateClubDeviceCallbackPayload" not necessary! */) => {
    // payload is known at compile time to be a SubscriptionOnCreateClubDeviceCallbackPayload
    ...
  }
});

这很令人兴奋,因为 amplify 库会检查

variables
字段是否定义了正确的字段,并且
next
函数仅引用其参数上存在的字段。

我的目标是能够将对

client.graphql(...).subscribe(...)
的调用包装在一个函数中,以
query
variables
next
值作为参数。根据设计,amplify-js 库的内部类型不会导出。

我会跳到我在做这件事时遇到的主要困难,然后慢慢地一点一点地讨论我是如何做到这一点的。我的症结在于

subscribe
的签名有一个参数,它是两种类型的并集,我希望我的包装函数仅采用其中一种:

Partial<Observer<NeverEmpty<OUTPUT_TYPE>>> | ((value: NeverEmpty<OUTPUT_TYPE>) => void)

我只会传递相当于

Partial<Observer<NeverEmpty<OUTPUT_TYPE>>>
的内容,因为这实际上包含
next
字段。我永远不会通过
(value: NeverEmpty<OUTPUT_TYPE>) => void
,无论如何它已被弃用。然而,我很难在包装函数上得到正确的签名,主要是因为
Observer
NeverEmpty
很复杂并且未从 amplify 导出。

首先,从 amplify-js 库复制的这段代码显示了参数最终如何类型安全地传递到库中:

/**
 * The expected return type with respect to the given `FALLBACK_TYPE`
 * and `TYPED_GQL_STRING`.
 */
export type GraphQLResponseV6<
    FALLBACK_TYPE = unknown,
    TYPED_GQL_STRING extends string = string,
> = TYPED_GQL_STRING extends GeneratedQuery<infer IN, infer QUERY_OUT>
    ? Promise<GraphQLResult<FixedQueryResult<QUERY_OUT>>>
    : TYPED_GQL_STRING extends GeneratedMutation<infer IN, infer MUTATION_OUT>
      ? Promise<GraphQLResult<NeverEmpty<MUTATION_OUT>>>
      : TYPED_GQL_STRING extends GeneratedSubscription<infer IN, infer SUB_OUT>
        ? GraphqlSubscriptionResult<NeverEmpty<SUB_OUT>>
        : FALLBACK_TYPE extends GraphQLQuery<infer T>
          ? Promise<GraphQLResult<FALLBACK_TYPE>>
          : FALLBACK_TYPE extends GraphQLSubscription<infer T>
            ? GraphqlSubscriptionResult<FALLBACK_TYPE>
            : FALLBACK_TYPE extends GraphQLOperationType<
                                infer IN,
                                infer CUSTOM_OUT
                >
              ? CUSTOM_OUT
              : UnknownGraphQLResponse;

现在,因为我是打字稿n00b,所以当我使用我刚刚学习的工具提取它们时,我慢慢地命名每种类型。这表明了我的想法:

type TypeOfClientGraphql<INPUT_TYPE, OUTPUT_TYPE> = typeof client.graphql<
  unknown /* FALLBACK_TYPES */,
  GeneratedSubscription<INPUT_TYPE, OUTPUT_TYPE> /* TYPED_GQL_STRING */
>;
// == GraphQLMethod<unknown, GeneratedSubscription<INPUT_TYPE, OUTPUT_TYPE>>

type ArgTypeOfClientGraphql<INPUT_TYPE, OUTPUT_TYPE> = Parameters<
  TypeOfClientGraphql<INPUT_TYPE, OUTPUT_TYPE>
>[0];
// == GraphQLOptionsV6<unknown, GeneratedSubscription<INPUT_TYPE, OUTPUT_TYPE>>

type NamedArgQueryTypeOfClientGraphql<INPUT_TYPE, OUTPUT_TYPE> =
  ArgTypeOfClientGraphql<INPUT_TYPE, OUTPUT_TYPE>["query"];
// == GeneratedSubscription<INPUT_TYPE, OUTPUT_TYPE>

type NamedArgVariablesTypeOfClientGraphql<INPUT_TYPE, OUTPUT_TYPE> =
  ArgTypeOfClientGraphql<INPUT_TYPE, OUTPUT_TYPE>["variables"];
// == GraphQLVariablesV6<unknown, GeneratedSubscription<INPUT_TYPE, OUTPUT_TYPE>>

type ReturnTypeOfClientGraphql<INPUT_TYPE, OUTPUT_TYPE> = ReturnType<
  TypeOfClientGraphql<INPUT_TYPE, OUTPUT_TYPE>
>;
// == GraphQLResponseV6<unknown, GeneratedSubscription<INPUT_TYPE, OUTPUT_TYPE>>
// == GraphqlSubscriptionResult<NeverEmpty<OUTPUT_TYPE>>
// == Observable<GraphqlSubscriptionMessage<NeverEmpty<OUTPUT_TYPE>>>

type TypeOfClientGraphqlSubscribe<INPUT_TYPE, OUTPUT_TYPE> =
  ReturnTypeOfClientGraphql<INPUT_TYPE, OUTPUT_TYPE>["subscribe"];
// == (observerOrNext?: Partial<Observer<NeverEmpty<OUTPUT_TYPE>>> | ((value: NeverEmpty<OUTPUT_TYPE>) => void)) => Subscription

type ArgTypeOfClientGraphqlSubscribe<INPUT_TYPE, OUTPUT_TYPE> = Parameters<
  TypeOfClientGraphqlSubscribe<INPUT_TYPE, OUTPUT_TYPE>
>[0];
// == Partial<Observer<NeverEmpty<OUTPUT_TYPE>>> | ((value: NeverEmpty<OUTPUT_TYPE>) => void)

现在我想了解

Partial<Observer<NeverEmpty<OUTPUT_TYPE>>>["next"]
,因为这将是我的包装函数上的参数类型:

export const errorCatchingSubscription = <INPUT_TYPE, OUTPUT_TYPE>({
  query,
  variables,
  next,
}: {
  query: NamedArgQueryTypeOfClientGraphql<INPUT_TYPE, OUTPUT_TYPE>;
  variables: NamedArgVariablesTypeOfClientGraphql<INPUT_TYPE, OUTPUT_TYPE>;
  next: ArgTypeOfClientGraphqlSubscribe<INPUT_TYPE, OUTPUT_TYPE>["next"];
}) => {
   ... 
}

但这不起作用,我认为因为

ArgTypeOfClientGraphqlSubscribe<INPUT_TYPE, OUTPUT_TYPE>["next"]
试图从联合类型
next
而不是类型
Partial<Observer<NeverEmpty<OUTPUT_TYPE>>> | ((value: NeverEmpty<OUTPUT_TYPE>) => void)
中提取
Partial<Observer<NeverEmpty<OUTPUT_TYPE>>>
字段的类型,并且类型
((value: NeverEmpty<OUTPUT_TYPE>) => void)
没有名为的字段“下一个”。

我尝试使用

Exclude<T, U>
实用程序,但由于
Observer
NeverEmpty
很复杂并且未从 amplify 导出,因此我无法明确且准确地说明我想要从联合中排除的函数类型。我也尝试过使用
infer
关键字,但是推断的类型会丢失关于
Partial<Observer<NeverEmpty<OUTPUT_TYPE>>>
的静态类型信息,它包含一个
next
字段,该字段是一个采用
OUTPUT_TYPE
的函数。

是否有其他方法可以在编译时静态地摆脱该

| ((value: NeverEmpty<OUTPUT_TYPE>) => void)
,以便我可以使用
["next"]
Partial<Observer<NeverEmpty<OUTPUT_TYPE>>>
中提取订阅通知回调的类型?否则我是否会做错所有事情并且有一些更好的方法来安全地包装这个调用?

typescript aws-amplify typescript-generics
1个回答
0
投票

我在另一篇文章中找到了解决我的困难的答案:看来我无法做我想做的事。

事实上,这个问题在类型链中出现的时间比我想象的要早。评论在:

type TypeOfClientGraphqlSubscribe<INPUT_TYPE, OUTPUT_TYPE> = ReturnTypeOfClientGraphql<INPUT_TYPE, OUTPUT_TYPE>["subscribe"]; // == (observerOrNext?: Partial<Observer<NeverEmpty<OUTPUT_TYPE>>> | ((value: NeverEmpty<OUTPUT_TYPE>) => void)) => Subscription
不正确。实际上,

subscribe

函数在
rxjs
所依赖的
aws-amplify
库中重载一次,而
(observerOrNext?: Partial<Observer<NeverEmpty<OUTPUT_TYPE>>> | ((value: NeverEmpty<OUTPUT_TYPE>) => void)) => Subscription
是两个签名中较早的一个。上面链接的帖子指出,
此打字稿文档明确指示最终签名是将由 ["subscribe"]
 运算符返回的类型,因此我有兴趣提取的类型 
Partial<...>
 根本不是都可用。

另外,郑重声明,当我在问题中说联合体的

(value: NeverEmpty<OUTPUT_TYPE) => void

 部分已被弃用时,我错了。即使在 rxjs v8 中,联合体的那部分
仍然是调用subscribe
方法
的推荐方式。只有第二个重载方法定义已被弃用,并将在 rxjs v8 中删除。一旦发生这种情况并被 amplify 消耗,过载就会消失,我将能够使用
["subscribe"]
 访问列出的联合类型而不是已弃用的类型,但我预计我仍然会遇到我认为的问题正如所提出的问题:我仍然无法在联合类型上使用 
Exclude<T, U>
,因为 
NeverEmpty
 不是从 amplify 导出的,并且不使用 
["next"]
Exclude<T, U>
 将失败,因为它只能应用于联盟的上半场。

© www.soinside.com 2019 - 2024. All rights reserved.