amplify-codegen icon indicating copy to clipboard operation
amplify-codegen copied to clipboard

Generated Typescript types are inconsistent

Open jayKayEss opened this issue 2 years ago • 2 comments

Before opening, please confirm:

  • [X] I have installed the latest version of the Amplify CLI (see above), and confirmed that the issue still persists.
  • [X] I have searched for duplicate or closed issues.
  • [X] I have read the guide for submitting bug reports.
  • [X] I have done my best to include a minimal, self-contained set of instructions for consistently reproducing the issue.
  • [X] I have removed any sensitive information from my code snippets and submission.

How did you install the Amplify CLI?

yarn

If applicable, what version of Node.js are you using?

16.13.0

Amplify CLI Version

7.6.20

What operating system are you using?

mac

Did you make any manual changes to the cloud resources managed by Amplify? Please describe the changes made.

No

Amplify Categories

api

Amplify Commands

codegen

Describe the bug

Using GraphQL and Opensearch, Amplify defines the following types in src/API.ts:

export type SearchableInvoiceConnection = {
  __typename: "SearchableInvoiceConnection",
  items:  Array<Invoice | null >,
  nextToken?: string | null,
  total?: number | null,
  aggregateItems:  Array<SearchableAggregateResult | null >,
};

export type SearchableAggregateResult = {
  __typename: "SearchableAggregateResult",
  name: string,
  result?: SearchableAggregateGenericResult | null,
};

export type SearchableAggregateGenericResult = SearchableAggregateScalarResult | SearchableAggregateBucketResult


export type SearchableAggregateScalarResult = {
  __typename: "SearchableAggregateScalarResult",
  value: number,
};

export type SearchableAggregateBucketResult = {
  __typename: "SearchableAggregateBucketResult",
  buckets?:  Array<SearchableAggregateBucketResultItem | null > | null,
};

export type SearchableAggregateBucketResultItem = {
  __typename: "SearchableAggregateBucketResultItem",
  key: string,
  doc_count: number,
};

Based on these, I should be able to write a type guard that I can use when iterating over the aggregates returned by OpenSearch to distinguish bucket results from scalar results:

export function isSearchableAggregateBucketResult(
  item: SearchableAggregateGenericResult | null | undefined
): item is SearchableAggregateBucketResult {
  return item !== null && item !== undefined && "buckets" in item;
}

However, this code produces an error:

TS2345: Argument of type '{ __typename: "SearchableAggregateScalarResult"; value: number; } | { __typename: "SearchableAggregateBucketResult"; buckets?: ({ __typename: string; key: string; doc_count: number; } | null)[] | null | undefined; } | null' is not assignable to parameter of type 'SearchableAggregateGenericResult | null | undefined'.

The reason for this is that the type of the OpenSearch results doesn't use any of the above types. Instead, it's typed this way:

export type SearchInvoicesQuery = {
  searchInvoices?:  {
    __typename: "SearchableInvoiceConnection",
    items:  Array< {
      __typename: "Invoice",
      id: string,
      supplier_paidol_id: string,
      supplier_paidol_name: string,
      accounts_receivable_name: string,
      date_sent: string,
      date_due: string,
      createdAt: string,
      sentAt?: string | null,
      status: InvoiceStatus,
      updatedAt: string,
    } | null >,
    nextToken?: string | null,
    total?: number | null,
    aggregateItems:  Array< {
      __typename: "SearchableAggregateResult",
      name: string,
      result: ( {
          __typename: "SearchableAggregateScalarResult",
          value: number,
        } | {
          __typename: "SearchableAggregateBucketResult",
          buckets?:  Array< {
            __typename: string,
            key: string,
            doc_count: number,
          } | null > | null,
        }
      ) | null,
    } | null >,
  } | null,
};

Although the typing references __typename: "SearchableInvoiceConnection", etc., the types are not actually composed here. This means that the type guard needs to be written like this:

export function isSearchableAggregateBucketResult(
  item:
    | { __typename: "SearchableAggregateScalarResult"; value: number }
    | {
        __typename: "SearchableAggregateBucketResult";
        buckets?:
          | ({ __typename: string; key: string; doc_count: number } | null)[]
          | null
          | undefined;
      }
    | null
    | undefined
): item is SearchableAggregateBucketResult {
  return item !== null && item !== undefined && "buckets" in item;
}

Expected behavior

API.ts should either use the types it defines, or those types should not be exposed to developers. They are not usable in their current form.

Reproduction steps

Define any @model @searchable in GraphQL and consume the results in Typescript.

GraphQL schema(s)

type Invoice
  @model(mutations: null, subscriptions: null)
  @auth(rules: [{ allow: public, provider: iam, operations: [read] }])
  @searchable {
  id: ID!
  supplier_paidol_id: ID!
  supplier_paidol: Paidol @hasOne(fields: ["supplier_paidol_id"])
  supplier_paidol_name: String!
  accounts_receivable_name: String!
  date_sent: AWSDateTime!
  date_due: AWSDateTime!
  balance_due: Currency
  createdAt: AWSDateTime!
  sentAt: AWSDateTime
  status: InvoiceStatus!
  items: [InvoiceItem] @hasMany(indexName: "byInvoice", fields: ["id"])
}

Log output

ERROR in src/store/invoicesSlice.ts:79:49

TS2345: Argument of type '{ __typename: "SearchableAggregateScalarResult"; value: number; } | { __typename: "SearchableAggregateBucketResult"; buckets?: ({ __typename: string; key: string; doc_count: number; } | null)[] | null | undefined; } | null' is not assignable to parameter of type 'SearchableAggregateGenericResult | null | undefined'.
  Type '{ __typename: "SearchableAggregateBucketResult"; buckets?: ({ __typename: string; key: string; doc_count: number; } | null)[] | null | undefined; }' is not assignable to type 'SearchableAggregateGenericResult | null | undefined'.
    Type '{ __typename: "SearchableAggregateBucketResult"; buckets?: ({ __typename: string; key: string; doc_count: number; } | null)[] | null | undefined; }' is not assignable to type 'SearchableAggregateBucketResult'.
      Types of property 'buckets' are incompatible.
        Type '({ __typename: string; key: string; doc_count: number; } | null)[] | null | undefined' is not assignable to type '(SearchableAggregateBucketResultItem | null)[] | null | undefined'.
          Type '({ __typename: string; key: string; doc_count: number; } | null)[]' is not assignable to type '(SearchableAggregateBucketResultItem | null)[]'.
            Type '{ __typename: string; key: string; doc_count: number; } | null' is not assignable to type 'SearchableAggregateBucketResultItem | null'.
              Type '{ __typename: string; key: string; doc_count: number; }' is not assignable to type 'SearchableAggregateBucketResultItem'.
                Types of property '__typename' are incompatible.
                  Type 'string' is not assignable to type '"SearchableAggregateBucketResultItem"'.
    77 |         .filter(isNotNull)
    78 |         .reduce<AggregatesMap>((accumulator, current) => {
  > 79 |           if (isSearchableAggregateBucketResult(current.result)) {
       |                                                 ^^^^^^^^^^^^^^
    80 |             accumulator[current.name] = (current.result.buckets ?? [])
    81 |               .filter(isNotNull)
    82 |               .map((item) => item.key)

Additional information

No response

jayKayEss avatar Feb 17 '22 20:02 jayKayEss