Using the API.ts Typescript types generated by AWS Amplify
We explore using Typescript with AWS Amplify API in React Native, with a particular focus on the types that AWS Amplify generates into API.ts
The example code in this post is very verbose, and performs extra checks, in order to explicitly show all the resulting types. Your production code can be much simpler, and can generalize out common code.
Mat Warger created a post on this issue, but it doesn’t cover the use of the generated types in API.ts
The example code for this post uses React Native 61.5 and AWS Amplify 2.2.1, and is at https://github.com/dantasfiles/TSAmplifyAPI
Initial Setup
Create a Typescript React Native Project> npx react-native init TSAmplifyAPI --template react-native-template-typescript
Install AWS Amplify> npm install aws-amplify
Initialize AWS Amplify> amplify init
Add AWS Amplify to index.js
// index.js
import Amplify from 'aws-amplify';
import config from './aws-exports';
Amplify.configure(config);
API Setup
Add the sample Todo API
> amplify add api
? Please select from one of the below mentioned services: GraphQL
? Provide API name: tsamplifyapi
? Choose the default authorization type for the API API key
? Enter a description for the API key:
? After how many days from now the API key should expire (1-365): 7
? Do you want to configure advanced settings for the GraphQL API No, I am done.
? Do you have an annotated GraphQL schema? No
? Do you want a guided schema creation? Yes
? What best describes your project: Single object with fields (e.g., “Todo” with ID, name, description)
Use AWS Amplify CodeGen to generate the GraphQL statements and types
> amplify configure codegen
? Choose the code generation language target typescript
? Enter the file name pattern of graphql queries, mutations and subscriptions src\graphql\**\*.ts
? Do you want to generate/update all possible GraphQL operations - queries, mutations and subscriptions Yes
? Enter maximum statement depth [increase from default if your schema is deeply nested] 2
? Enter the file name for the generated code src\API.ts
? Do you want to generate code for your newly created GraphQL API Yes
√ Generated GraphQL operations successfully and saved at src\graphql
√ Code generated successfully and saved in file src\API.ts
Note: Feel free to open up and explore the
src/graphql
andsrc/API.ts
files before continuing.
Add your API to App.tsx
Import the generated GraphQL statements and types
// App.tsx
import API, {GraphQLResult, graphqlOperation}
from '@aws-amplify/api';
import {listTodos, getTodo} from './src/graphql/queries';
import {createTodo, updateTodo, deleteTodo}
from './src/graphql/mutations';
import * as APIt from './src/API';
Note: I imported
API
from@aws-amplify/api
because I ran into typescript issues whereAPI.GraphQLResult = any
if I instead usedimport {API} from 'aws-amplify'
We import the generated GraphQL statements from src/graphql/queries.ts
and src/graphql/mutations.ts
We import the generated types for our API from src/API.ts
Create a typescript type for your GraphQL type
No type for Todo
objects is generated in API.ts
, but having one is very useful in our code.
For simple schemas without @connection
annotations, we use a technique from Steve Lizcano’s post to generate a Todo
type automatically from the return type of the getTodo
graphQL query, which is:
// API.ts
export type GetTodoQuery = {
getTodo: {
__typename: "Todo",
id: string,
name: string,
description: string | null,
} | null,
};
We use the Omit and Exclude utility types in App.tsx
to remove the null
and __typename
// App.tsx
type Todo = Omit<Exclude<APIt.GetTodoQuery['getTodo'], null>,
'__typename'>;
to get a resulting type of :
type Todo = {
id: string,
name: string,
description: string | null,
};
This Todo
type comes in useful, and we use it extensively in our example code.
Note: Since this was written, AWS added a new
DataStore
command for React,amplify codegen models
, that generates anmodels/index.d.ts
file with aTodo
class that may also be useful.
// models/index.d.ts
export declare class Todo {
readonly id: string;
readonly name: string;
readonly description?: string;
constructor(init: ModelInit<Todo>);
static copyOf(
source: Todo,
mutator: (draft: MutableModel<Todo>) => MutableModel<Todo> | void
): Todo;
}
Since this post is mainly concerned with the
API
package and React Native, I haven’t yet fully explored this newamplify codegen models
command.
Run a Create GraphQL Mutation
// App.tsx
async function accessAPI() {
let id: string = '';
const createI: APIt.CreateTodoInput = {
name: 'create name',
description: 'create description',
};
const createMV: APIt.CreateTodoMutationVariables = {
input: createI,
};
const createR: GraphQLResult<APIt.CreateTodoMutation> =
await API.graphql(graphqlOperation(createTodo, createMV));
if (createR.data) {
const createTM: APIt.CreateTodoMutation = createR.data;
if (createTM.createTodo) {
const todo: Todo = createTM.createTodo;
console.log('CreateTodo', todo);
id = createTM.createTodo.id;
}
}
...
As stated earlier, this code is very verbose, and performs extra checks, in order to explicitly show all the resulting types. Your production code can be much simpler, and can generalize out common code.
First, we create a CreateTodoInput
that specifies the Todo
to be created. Second, we create a CreateTodoMutationVariables
that will be our input to the createTodo
GraphQL Mutation. CreateTodoMutationVariables
can also contain an optional condition
.
Third, we run the createTodo
GraphQL mutation and get a GraphQLResult<APIt.CreateTodoMutation>
result.
Note: my typescript compiler thinks the return type of
API.graphql
isany
, so I explicitly cast the result toGraphQLResult<APIt.CreateTodoMutation>
which works fine.
Fourth, we check for nulls to access the CreateTodoMutation
result.
Fifth, we check for nulls to access the created Todo
object.
Finally, we save the id
of the created Todo
object for later use.
Update GraphQL Mutation
// App.tsx
const updateI: APIt.UpdateTodoInput =
{id, description: 'update description'};
const updateMV: APIt.UpdateTodoMutationVariables = {
input: updateI,
};
const updateR: GraphQLResult<APIt.UpdateTodoMutation> =
await API.graphql(graphqlOperation(updateTodo, updateMV));
if (updateR.data) {
const updateTM: APIt.UpdateTodoMutation = updateR.data;
if (updateTM.updateTodo) {
const todo: Todo = updateTM.updateTodo;
console.log('UpdateTodo:', todo);
}
}
The updateTodo
GraphQL mutation works similarly to the createTodo
GraphQL mutation. We use the id
that we received from the previous createTodo
GraphQL mutation.
Get GraphQL Query
// App.tsx
const getQV: APIt.GetTodoQueryVariables = {id};
const getGQL: GraphQLResult<APIt.GetTodoQuery> =
await API.graphql(graphqlOperation(getTodo, getQV));
if (getGQL.data) {
const getQ: APIt.GetTodoQuery = getGQL.data;
if (getQ.getTodo) {
const todo: Todo = getQ.getTodo;
console.log('GetTodo:', todo);
}
}
First, we create a GetTodoQueryVariables
input to the getTodo
GraphQL query. We use the id
that we received from the previous createTodo
GraphQL mutation.
Second, we get the GraphQLResult<APIt.GetTodoQuery>
result of the getTodo
GraphQL query.
Third, we check for null to access the APIt.GetTodoQuery
result.
Finally, we check for null to access the Todo
object that was the result of the getTodo
GraphQL query.
List GraphQL Query
// App.tsx
const listQV: APIt.ListTodosQueryVariables = {};
const listGQL: GraphQLResult<APIt.ListTodosQuery> =
await API.graphql(graphqlOperation(listTodos, listQV));
if (listGQL.data) {
const listQ: APIt.ListTodosQuery = listGQL.data;
if (listQ.listTodos && listQ.listTodos.items) {
listQ.listTodos.items.forEach((item: Todo | null) => {
if (item) {
const todo: Todo = item;
console.log('ListTodo:', todo);
}
});
}
}
The listTodos
GraphQL query works similarly to the getTodo
GraphQL query. Our ListTodosQueryVariables
is empty, but it can contain optional filter
, limit
, and nextToken
fields.
There’s some subtlety here in that the type of listQ.listTodos.items
is Array<Todo | null> | null
Delete GraphQL Mutation
// App.tsx
const deleteI: APIt.DeleteTodoInput = {id};
const deleteMV: APIt.DeleteTodoMutationVariables = {
input: deleteI,
};
const deleteR: GraphQLResult<APIt.DeleteTodoMutation> =
await API.graphql(graphqlOperation(deleteTodo, deleteMV));
if (deleteR.data) {
const deleteTM: APIt.DeleteTodoMutation = deleteR.data;
if (deleteTM.deleteTodo) {
const todo: Todo = deleteTM.deleteTodo;
console.log('DeleteTodo:', todo);
}
}
The deleteTodo
GraphQL mutation works similarly to the createTodo
and updateTodo
GraphQL mutations. We use the id
that we received from the previous createTodo
GraphQL mutation.
Testing the API
Run the mock API
> amplify mock api
GraphQL schema compiled successfully.
Creating table TodoTable locally
Running GraphQL codegen
√ Generated GraphQL operations successfully and saved at src\graphql
√ Code generated successfully and saved in file src\API.ts
AppSync Mock endpoint is running at http://localhost:20002
Run your app> npx react-native run-android
Your console.log
will be something like
LOG Running "TSAmplifyAPI" with {"rootTag":XX}
LOG CreateTodo {"description": "create description", "id": "XXXX-XXXX-XXXX-XXXX-XXXX", "name": "create name"}
LOG UpdateTodo: {"description": "update description", "id": "XXXX-XXXX-XXXX-XXXX-XXXX", "name": "create name"}
LOG GetTodo: {"description": "update description", "id": "XXXX-XXXX-XXXX-XXXX-XXXX", "name": "create name"}
LOG ListTodo: {"description": "update description", "id": "XXXX-XXXX-XXXX-XXXX-XXXX", "name": "create name"}
LOG DeleteTodo: {"description": "update description", "id": "XXXX-XXXX-XXXX-XXXX-XXXX", "name": "create name"}
Connections
The example Todo
GraphQL schema is simple: it has no @connection
annotations. For more complex GraphQL schemas with @connection
annotations, you’ll need to take some extra steps.
First, make sure that the maximum statement depth
for amplify configure codegen
is large enough that the types in API.ts
are expanded enough for your needs.
Next, when you jump across connections, take special care to watch out for all the | null
types that AWS Amplify adds. For example, in a Blog and Post schema (no Comments for simplicity), the getBlock
result type looks like this.
export type GetBlogQuery = {
getBlog: {
__typename: "Blog",
id: string,
name: string,
posts: {
__typename: "ModelPostConnection",
items: Array< {
__typename: "Post",
id: string,
title: string,
} | null > | null,
nextToken: string | null,
} | null,
} | null,
};
You’ll have to pay particular attention to dealing with the highlighted | null
’s when your query connects from a Blog to its Posts.
Subscriptions
Note: I haven’t experimented with subscriptions in Typescript. If that changes, I’ll update this section.