Is your feature request related to a problem? Please describe.
When I try to await
an API.graphql
call in TS, I get this:
This is because you have defined the graphql
method to also return an Observable
in case of a subscription:
This is understandable but makes it very annoying to use, particularly because if i need to assert types, the GraphQLResult
type is not also exported for me to use.
Describe the solution you'd like
use overloading and/or a different function for subscriptions to make this experience easier.
ultimately i want to be able to write this:
const query = await API.graphql<TodoType[]>({query: listTodos})
and it should not complain. (a REALLY good experience, ofc, would give me that return type for free based on listTodos
, but that would involve a much deeper redesign)
Secondly, specifying the return signature like this:
always assigns the generic type T
to be object
because that is how the GraphQLResult
interface is designed. This causes nasty, nasty type errors when people try to assign the results of the graphql query to a properly typed variable:
more info on this error here: https://stackoverflow.com/questions/56505560/could-be-instantiated-with-a-different-subtype-of-constraint-object
but TLDR:
i would recommend making graphql
a generic as well so I can pass along the type i want:
// pseudocode, modified from source slightly
graphql<T = object>(
options: GraphQLOptions,
additionalHeaders?: { [key: string]: string }
): Promise<GraphQLResult<T>> | Observable<object> {
return this._graphqlApi.graphql(options, additionalHeaders);
}
for those finding this.. heres how to write a vaguely ergonomic wrapper around this.
import { GraphQLOptions} from '@aws-amplify/api-graphql'
async function gql<T extends object>(options: GraphQLOptions, additionalHeaders?: {
[key: string]: string;
}): Promise<GraphQLResult<T>> {
const query = API.graphql(options, additionalHeaders) as Promise<GraphQLResult<T>>
return query
}
usage:
const [todos, setTodos] = React.useState<TodoType[]>([])
React.useEffect(() => {
const query = gql<TodoType[]>({query: listTodos})
query.then(({data}) => setTodos(data || []))
})
even specifying the input
s are quite verbose so i have modified the wrapper as such:
async function gql<T extends object>(
query: string,
variables?: object,
additionalHeaders?: {
[key: string]: string;
}
): Promise<GraphQLResult<T>> {
setLoading(true);
const q = (await API.graphql(
{
query,
variables: variables && { input: variables },
},
additionalHeaders
)) as Promise<GraphQLResult<T>>;
setLoading(false);
return q;
}
Thanks @sw-yx !
This is a great write-up, as you pointed out, with the current function signature it is not possible to unambiguously tell for sure if you want a promise (for queries/mutations) or an Observable (for subscriptions). This is due to the fact that the return type is not known at compile time, only at runtime (by looking at the operation type: query, mutation or subscription)
A wrapper similar to the one you propose might be the best course of action for now while we explore a deeper redesign. And of course, exporting the GraphQLResult
type
if you are open to deeper redesign, i think my dream API is:
const query = await API.graphql(listTodos, { variables, additionalHeaders })
with query
's type properly inferred because I used listTodos
otherwise the mismatches between the user-specified TS types and the GraphQL/DataStore schema are super annoying to have to manually maintain. (i understand this is an unrealistic dream right now)
maybe a super drastic redesign would look like
const query = await listTodos({ variables, additionalHeaders })
and the codegen would handle all that api.graphql stuff
Thanks @sw-yx for opening this. The ergonomics of using typescript is very rough out of the box. Having used apollo before, being able to pass the result and variable types into the generic query function seems like a must.
otherwise the mismatches between the user-specified TS types and the GraphQL/DataStore schema are super annoying to have to manually maintain. (i understand this is an unrealistic dream right now)
Not having to pass in the types at all would be amazing!
related thoughts from @manueliglesias in an old comment: https://github.com/aws-amplify/amplify-js/issues/3704#issuecomment-514769571
i also noticed that the API.ts
autogenerated types dont seem usable but i am not sure why:
import { API } from 'aws-amplify';
import { listBlogs } from "./graphql/queries";
import { createBlog } from "./graphql/mutations";
import { ListBlogsQuery } from "./API"; /// <----- the problematic type
type Blog = {
title: string;
image: string;
body: string;
id?: string;
createdAt?: Date;
updatedAt?: Date;
};
function App() {
const [blogs, setBlogs] = React.useState<Blog[]>([]);
const [currentSelectedBlog, setCurrentSelectedBlog] = React.useState<Blog>();
React.useEffect(fetchBlogs);
function fetchBlogs() {
const query = API.graphql({ query: listBlogs }) as Promise<{data: ListBlogsQuery}>
query.then(
({
data: {
listBlogs: { items }, // <--- type error here
},
}) => setBlogs(items)
);
}
This is how I use the API with typescript, if you want, you can customize it
import { API, graphqlOperation } from 'aws-amplify';
import { GraphQLOptions } from '@aws-amplify/api-graphql';
class CustomApi {
public async query<R extends any | any[], V = GraphQLOptions['variables']>(query: string, variables?: V) {
try {
return await this.makeRequest<R extends any[] ? { items: R; nextToken: string | null } : R>(query, variables);
} catch (err) {
console.error(err);
throw new Error('Api query error');
}
}
public async mutate<V extends GraphQLOptions['variables'], R = any>(query: string, variables: V) {
try {
return await this.makeRequest<R>(query, variables);
} catch (err) {
console.error(err);
throw new Error('Api mutate error');
}
}
private async makeRequest<R>(query: string, variables?: any): Promise<R> {
const { data } = await API.graphql(graphqlOperation(query, variables)) as any;
return data[Object.keys(data)[0]];
}
}
Most helpful comment
if you are open to deeper redesign, i think my dream API is:
with
query
's type properly inferred because I usedlistTodos
otherwise the mismatches between the user-specified TS types and the GraphQL/DataStore schema are super annoying to have to manually maintain. (i understand this is an unrealistic dream right now)
maybe a super drastic redesign would look like
and the codegen would handle all that api.graphql stuff