Using the API.ts Typescript types generated by AWS Amplify

Daniel Dantas (@dantasfiles)
7 min readJan 9, 2020

--

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 and src/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 where API.GraphQLResult = any if I instead used import {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 an models/index.d.ts file with a Todo 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 new amplify 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 is any, so I explicitly cast the result to GraphQLResult<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.

--

--

Daniel Dantas (@dantasfiles)

I create guides to help me fully understand the issues that I’m encountering and fixing. Web: dantasfiles.com Email: daniel@dantasfiles.com