Amplify JS is looking to improve our TypeScript support across the library to better serve our customers and provide a more intuitive & idiomatic developer experience. To this end, we are requesting your feedback on a variety of changes and improvements that will be available to you in our next major version release.
This RFC is broken down into sections covering:
- Library-wide TypeScript improvements
- Changes to some of our core utilities such as
Hub
and Amplify configuration - Specific improvements to the
Auth
,Storage
, andAPI
categories and associated APIs
We're also requesting feedback on any other TypeScript issues or pain points that you may have encountered not explicitly covered in this RFC.
Amplify JS will be making the following improvements to our TypeScript support. These improvements will be applied across the entire library, not just the categories highlighted below.
strict
typings — We will be applyingstrict
mode to the entire library to improve the usability of our types. This will allow you to more easily construct API requests, avoid errors, and have higher confidence when handling API responses. Amplify JS will just work with your application without any additional configurations if you have strict mode on.- Better runtime error typing — We will provide utilities for asserting type information of runtime errors emitted by Amplify.
- Upgraded TypeScript version — We will be upgrading the version of TypeScript that Amplify uses and provide explicit type definitions for developers using older versions. This will provide a variety benefits such as removing the need to specify
skipLibCheck
when using Amplify with newer versions of TypeScript.
Related issues:
- Could not compile library when strict mode is enabled in typescript (#7188)
- Typescript 4.9.4 compatibility issue with catch(err) in Storage module (#10824)
- Cannot use namespace 'Observable' as a type (#9204)
- New Angular 15 Apps don't build due to TS error (#10775)
Amplify is proposing the following changes to our core utilities.
We are improving developer experience by adding strict type support to Hub channels, events, and payloads. An example of the developer experience when listening for auth
events is highlighted below.
Amplify Channel Usage
Hub.listen('auth', ({ payload }) => {
switch (payload.event) {
case 'signInFailure':
const data = payload.data;
break;
}
});
Current DX (v5)
Proposed DX (v6)
We are improving developer experience by adding strict type support to custom Hub channels, events, and payloads.
Current Usage (v5)
const customChannel = "custom_channel";
const customEvent = "custom_event";
const customData = "custom_data";
Hub.dispatch(channel, {
event: customEvent,
data: customData
});
Hub.listen(channel, ({ payload }) => {
switch (payload.event) {
case customEvent:
const data = payload.data;
break;
}
});
Proposed Usage (v6)
type CustomEventData =
| { event: "A"; data: number }
| { event: "B"; data: string }
| { event: "C" }
| { event: "D"; data: object };
type CustomChannelMap = {
channel: "custom_channel";
eventData: CustomEventData;
};
Hub.dispatch<CustomChannelMap>("custom_channel", { event: "A", data: 1 });
Hub.listen<CustomChannelMap>("custom_channel", ({ payload }) => {
switch (payload.event) {
case "A":
payload.data;
break;
case "B":
payload.data;
break;
case "C":
// Type C doesn't have any associated event data
// @ts-expect-error
data = payload.data
break;
case "D":
payload.data;
break;
}
});
Related issue: Fully typed hubs (#5997)
To help developers configure Amplify categories, we are introducing type support for the Amplify.configure
API. This will allow you to easily setup your AWS resources if you are connecting Amplify JS to resources you have not created directly with the Amplify CLI. The examples below demonstrate an Auth
configuration.
Current Usage (v5)
const authConfig = {
userPoolId: 'us-east-1_0yqxxHm5q',
userPoolClientId: '3keodiqtm52nhh2ls0vQfs5v1q',
signUpVerificationMethod: 'code'
};
Amplify.configure({
Auth: authConfig
});
Proposed Usage (v6)
const authConfig : AuthConfig = {
userPoolId: 'us-east-1_0yqxxHm5q',
userPoolClientId: '3keodiqtm52nhh2ls0vQfs5v1q',
signUpVerificationMethod: 'code'
};
Amplify.configure({
Auth: authConfig
});
Related issue: A suggestion regarding typings for the Amplify.configure() function (#5095)
Try out the proposed types here: https://stackblitz.com/edit/rfc-typescript-v6?file=examples-core.ts
Amplify is proposing the following changes for the Auth
category. Similar changes will be applied across all of the Auth
APIs but examples for specific APIs are highlighted below.
User attributes inference on the signUp
API.
Current Usage (v5)
Auth.signUp({
username: 'username',
password: '*******',
attributes: {
email: '[email protected]'
}
});
Proposed Usage (v6)
Auth.signUp({
username: "username",
password: "*******",
options: {
userAttributes: {
email: "[email protected]",
},
},
});
Related issue: TypeScript definition not matching: Property 'attributes' does not exist on type 'CognitoUser' (#9941)
We are improving DX by providing descriptive API responses to help developers complete auth flows. An example for the confirmSignUp
API is highlighted below.
Current Usage (v5)
const resp = await Auth.confirmSignUp('username', '112233')
if (resp === 'SUCCESS'){
// Show login component
}
Proposed Usage (v6)
const resp = await confirmSignUp({
username: 'username',
confirmationCode: '112233',
});
if (resp.isSignUpComplete) {
// Show login component
}
Related issues:
- Update types for Promises on Auth calls (#9286)
- authenticator: add key types to user session payload (#10142)
- Return types of functions in AuthClass are unsafe (#6053)
Try out the proposed types here: https://stackblitz.com/edit/rfc-typescript-v6?file=examples-auth.ts
Amplify is proposing the following changes for the Storage
category.
In order to permit better interoperability between storage
APIs we will introduce StorageObjectReference
& StoragePrefixReference
types to represent items in cloud storage. An example for copying an object from one access level to another is highlighted below.
Current Usage (v5)
// List all public photos
const listResponse = await Storage.list('photos/', { level: 'public' });
const firstPhoto = listResponse.results?.[0];
// Copy the first photo returned to the current user's private prefix
if (firstPhoto) {
await Storage.copy({
{
key: firstPhoto.key,
level: 'public'
},
{
key: firstPhoto.key,
level: 'private'
}
})
}
Proposed Usage (v6)
// New reference types (full types available in the sandbox)
type StorageObjectMetadata = {
readonly size?: number;
readonly eTag?: string;
readonly lastModified?: Date;
};
type StorageObjectReference = {
readonly key: string;
readonly metadata?: StorageObjectMetadata;
} & AccessLevelConfig;
type StoragePathReference = {
readonly path: string;
} & AccessLevelConfig;
// List all public photos
const listResponse = await Storage.list({
path: getPathReference('photos/', { level: 'public' })
})
const firstPhoto = listResponse.files?.[0];
/*
As a note, APIs will allow developers to specify keys by string if they do not need to override the access level. For
example, the following operation will list all files for the current user.
*/
const listResponseDefault = await Storage.list({
path: 'photos/'
})
// Copy the first photo returned to the current user's private prefix
if (firstPhoto) {
await copy({
source: firstPhoto,
destination: copyObjectReference(firstPhoto, { level: 'private' }),
});
}
To better capture customer intent and simplify API types we will split up the get
API into getUrl
& download
. An example for generating a pre-signed URL & downloading a file from the results of a list
operation is highlighted below.
Current Usage (v5)
// List public photos
const listResponse = await Storage.list('photos/', { level: 'public' });
const firstPhoto = listResponse.results?.[0];
// Generate a pre-signed URL for a file
const presignedUrl = await Storage.get(firstPhoto.key, { level: 'public' });
// Download a file
const downloadResult = await Storage.get(firstPhoto.key, { download: true, level: 'public' });
Proposed Usage (v6)
// List public photos
const listResponse = await Storage.list({
path: getPathReference('photos/', { level: 'public' })
})
const firstPhoto = listResponse.files?.[0];
// Generate a pre-signed URL for a file
const presignedUrl = await Storage.getUrl({ key: firstPhoto });
// Download a file
const downloadResult = await Storage.download({ key: firstPhoto });
To better capture customer intent the put
API will be renamed to upload
. Additionally upload
will enable resumability by default in order to simplify API usage and remove the need to provide callbacks for monitoring upload status in favor of a Promise.
Current Usage (v5)
// Upload a public file with resumability enabled
const uploadTask = Storage.put('movie.mp4', fileBlob, {
resumable: true,
level: 'public',
progressCallback: (progress) => {
// Progress of upload
},
completeCallback: (event) => {
// Upload finished
},
errorCallback: (err) => {
// Upload failed
}
});
// Pause & resume upload
uploadTask.pause();
uploadTask.resume();
Proposed Usage (v6)
// Upload a public file with resumability enabled by default
const uploadTask = Storage.upload({
key: getObjectReference('movie.mpg', { level: 'public' }),
content: fileBlob
});
// Pause & resume upload
let currentTransferStatus = uploadTask.pause();
currentTransferStatus = uploadTask.resume();
// Get the current progress of the upload
const currentTransferProgress = uploadTask.getProgress();
// Wait for the upload to finish (or fail)
const uploadedObjectReference = await uploadTask.result;
Try out the proposed storage
types here: https://stackblitz.com/edit/rfc-typescript-v6?file=examples-storage.ts
Amplify is proposing the following changes for the GraphQL API
category to improve type safety and readability.
To better capture customer intent and simplify API types we will introduce dedicated APIs for queries, mutations, and subscriptions. We're going to retain the graphql()
operation in case you want to issue multiple queries/mutations in a single request.
Current Usage (v5)
const todoDetails: CreateTodoInput = {
name: "Todo 1",
description: "Learn AWS AppSync",
};
const newTodo = await API.graphql<GraphQLQuery<CreateTodoMutation>>({
query: mutations.createTodo,
variables: { input: todoDetails },
});
const subscription = API.graphql<GraphQLSubscription<OnCreateTodoSubscription>>(
graphqlOperation(subscriptions.onCreateTodo)
).subscribe({
next: ({ provider, value }) => console.log({ provider, value }),
error: (error) => console.warn(error),
});
Proposed Usage (v6)
type MyQueryType = {
variables: {
filter: {
id: number;
};
};
result: {
listTodos: {
items: {
id: number;
name: string;
description: string;
}[];
};
};
};
const result = await API.query<MyQueryType>("query lisTodos...", {
filter: { id: 123 },
});
console.log(`Todo : ${result.listTodos[0].name})`);
type MyMutationType = {
variables: {
input: {
id: number;
name: string;
description: string;
};
};
result: {
createTodo: {
id: number;
name: string;
description: string;
};
};
};
const result = await API.mutate<MyMutationType>("mutation createTodo....", {
input: {
id: 123,
name: "My Todo",
description: "This is a todo",
},
});
console.log(
`Todo : ${result.createTodo.id} ${result.createTodo.name} ${result.createTodo.description})`
);
type MySubscriptionType = {
variables: {
filter: {
name: {
eq: string;
};
};
};
result: {
createTodo: {
id: number;
name: string;
description: string;
};
};
};
API.subscribe<MySubscriptionType>("subscription OnCreateTodo...", {
filter: {
name: { eq: "awesome things" },
},
}).on({
next: (result) => console.log(`Todo info: ${result.createTodo.name})`),
});
In v6, we want to reduce the verbosity of the typings for the code-generated queries, mutations, and subscriptions by inferring their types from the generated code.
Current Usage (v5)
import { API, graphqlOperation } from "aws-amplify";
import { GraphQLQuery, GraphQLSubscription } from "@aws-amplify/api";
import { createTodo } from "./graphql/mutations";
import { onCreateTodo } from "./graphql/subscriptions";
import {
CreateTodoInput,
CreateTodoMutation,
OnCreateTodoSubscription,
} from "./API";
// so many imports from disparate modules
function createMutation() {
const createInput: CreateTodoInput = {
name: "Improve API TS support",
};
// Verbose explicit type definition when the information could be available in `createTodo`'s type
const res = await API.graphql<GraphQLQuery<CreateTodoMutation>>(
graphqlOperation(createTodo, {
input: createInput,
})
);
// the returned data is nested 2 levels deep and could be upleveled when the GraphQL document
// only includes one query or mutation
const newTodo = res.data?.createTodo;
}
function subscribeToCreate() {
const sub = API.graphql<GraphQLSubscription<OnCreateTodoSubscription>>(
graphqlOperation(onCreateTodo)
).subscribe({
next: (message) => {
// once again, we could "sift up" the return value instead of providing it in two levels of depth
const newTodo = message.value?.data?.onCreateTodo;
},
});
}
Proposed Usage (v6)
import { API } from 'aws-amplify';
import { createTodo } from './graphql/mutations';
import { onCreateTodo } from './graphql/subscriptions';
import { CreateTodoInput } from './API';
// so many imports from disparate modules
function createMutation() {
const createInput: CreateTodoInput = {
name: 'Improve API TS support ',
};
const res = await API.mutate(createTodo, {
input: createInput,
});
// The returned data is the result of the request. If there are more than one queries/mutations in a request,
// then the return value stays the same as v5. i.e. res.createTodo.data
const newTodo = res;
}
function subscribeToCreate() {
const sub = API.subscribe(onCreateTodo).on({
next: (message) => {
// Return value shortened slightly from `message?.data?.onCreateTodo`.
next: (message) => {
console.log(message.onCreateTodo);
},
}
});
}
As alluded to in the previous section, we're looking to flatten the results of GraphQL operations to make them more easily accessible instead of the current three-levels-deep nested object. Would love to get your understanding on which option you prefer.
Current Usage (v5)
async function createNewTodo() {
const res: GraphQLResult<Todo> = await API.graphql<GraphQLQuery<Todo>>(
graphqlOperation(createTodo, {
input: { id: uuid() },
})
);
// Mutation result is nested
console.log(res.data.createTodo);
}
Proposed Option 1: Flatten to the lowest level (v6)
async function createNewTodo() {
const res: Todo = await API.mutate(createTodo, {
input: { id: uuid() },
});
// Response flattened to the todo level
console.log(res);
}
Proposed Option 2: Flatten to the data
level (v6)
async function createNewTodo() {
const res = await API.mutate(createTodo, {
input: { id: uuid() },
});
// Response flattened to the `data` level
console.log(res.createTodo);
}
interface GraphQLData<T = object> {
[query: string]: T; // in the above example T is Todo
}
In GraphQL, you can define multiple queries or mutations in a single request via the .graphql()
operation. The response object will include the result of all the queries and mutations. For example, given the following queries:
async function custom() {
type MyMultiQueryType = {
variables: {
input: {
todoId: string;
fooId: string;
};
};
result: {
getTodo: {
id: number;
name: string;
createdAt: Date;
updatedAt: Date;
};
getFoo: {
id: number;
name: string;
createdAt: Date;
updatedAt: Date;
};
};
};
const operation = {
query: `
query GetTodo($todoId: ID!, $fooId: ID!) {
getTodo(id: $todoId) {
id
name
createdAt
updatedAt
}
getFoo(id: $fooId) {
id
name
createdAt
updatedAt
}
}
`,
variables: {
todoId: "c48481bd-f808-426f-8fed-19e1368ca0bc",
fooId: "9d4e6e30-fcb4-4409-8160-7d44931a6a02",
},
authToken: undefined,
userAgentSuffix: undefined,
};
const result = await API.graphql<MyMultiQueryType>(operation.query, {
variables: operation.variables,
});
}
Proposed Option 1: Flatten to data level
console.log(res.getTodo);
console.log(res.getFoo);
Proposed Option 2: Flatten to the array level
// retains the ordering of the queries in the graphql request
console.log(res[0]); // todo
console.log(res[1]); // foo
Proposed Option 3: Don't flatten at all
console.log(res.data.getTodo);
console.log(res.data.getFoo);
In v6, we want to ensure type safety on GraphQL inputs if you use one of the generated GraphQL queries, mutations, or subscriptions.
Current Usage (v5)
import { updateTodo } from "./graphql/mutations";
import { CreateTodoInput } from "./API";
const createInput: CreateTodoInput = {
name: todoName,
description,
};
const res = await API.graphql<GraphQLQuery<UpdateTodoMutation>>(
graphqlOperation(updateTodo, {
// passing an object of type CreateTodoInput (that's missing
// a required field for updates `id`) into an update mutation's input
// does not surface a type error. This will only throw a runtime error after
// the mutation request gets rejected by AppSync
input: createInput,
})
);
Proposed Usage (v6)
import { updateTodo } from "./graphql/mutations";
import { CreateTodoInput } from "./API";
const createInput: CreateTodoInput = {
name: todoName,
description,
};
const res = await API.mutate(updateTodo, {
// @ts-expect-error
input: createInput, // `input` must be of type `UpdateTodoInput`
});
Currently there's a bug in which the generated API types contain __typenames
but not in the selection set of the generated GraphQL operations. This causes runtime type checking errors when you rely on TypeScript to expect the "__typename" field to be present but it isn't. Prior to the v6 launch, we'll fix this bug to ensure the type definition matches the selection set/return value of the GraphQL operation during runtime.
In v5, there's a type mismatch bug for GraphQL subscriptions that forces the developer to cast to any
to subscribe or unsubscribe. We plan on fixing this for v6.
Current Usage (v5)
import { onCreateUser } from './graphql/subscriptions'
const subscription.value = (API.graphql(graphqlOperation(
onCreateUser,
{ id: userId }
)) as any).subscribe({ next: onSubscribe })
(subscription.value as any).unsubscribe()
Proposed Usage (v6)
import { onCreateUser } from "./graphql/subscriptions";
const subscription = API.subscribe(onCreateUser, { id: userId }).on({
next: onSubscribe,
});
// . . .
subscription.unsubscribe();
Amplify is proposing the following changes for the REST API
category.
To improve the readability of our APIs we will be introducing an object parameter to capture request parameters.
Current Usage (v5)
const apiName = "MyApiName";
const path = "/path";
const myInit = {
headers: {}, // OPTIONAL
response: true, // OPTIONAL (return the entire Axios response object instead of only response.data)
queryStringParameters: {
name: "param", // OPTIONAL
},
};
const result = await API.get(apiName, path, myInit);
Proposed Usage (v6)
await API.get(
{
apiName: "MyApi",
path: "/items",
authMode: "AWS_IAM",
},
{
headers: {
"custom-header": "x",
},
}
);
To improve developer experience and permit more strict typing we will be adding generic support to our API
category APIs.
Current Usage (v5)
Amplify v5 does not support using generics for the request body or response.
Proposed Usage (v6)
type MyApiResponse = { firstName: string; lastName: string };
const result = await API.get<MyApiResponse>({
apiName: "MyApi",
path: "/getName",
});
console.log(`The name is ${result.body.firstName} ${result.body.lastName}`);
const result = await API.put<string, { data: Array<number> }>(
{
apiName: "",
path: "/",
authMode: "API_KEY",
},
{
headers: {
"Content-type": "text/plain",
},
body: "this is my content",
}
);
result.body.data.forEach((value) => console.log(value));
Current Usage (v5)
Amplify v5 does not support narrowing down errors.
Proposed Usage (v6)
try {
await API.get({
apiName: "myApi",
path: "/",
});
} catch (err: unknown) {
if (err instanceof API.NetworkError) {
// Consider retrying
} else if (err instanceof API.HTTPError) {
// Check request parameters for mistakes
} else if (err instanceof API.CancelledError) {
// Request was cancelled
} else if (err instanceof API.BlockedError) {
// CORS related error
} else {
// Other error
}
}
Try out the proposed api
types here: https://stackblitz.com/edit/rfc-typescript-v6?file=examples-api.ts