Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: Amplify JS TypeScript Improvements #11113

Closed
jimblanc opened this issue Mar 21, 2023 · 26 comments
Closed

RFC: Amplify JS TypeScript Improvements #11113

jimblanc opened this issue Mar 21, 2023 · 26 comments
Assignees
Labels
Feedback Feedback on our library TypeScript Related to TypeScript issues

Comments

@jimblanc
Copy link
Contributor

RFC: Amplify JS TypeScript Improvements

This issue is a Request For Comments (RFC). It is intended to elicit community feedback regarding a proposed change to the library. Please feel free to post comments or questions here.

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, and API 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.

Library-wide TypeScript Improvements

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 applying strict 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:

Utility Changes

Amplify is proposing the following changes to our core utilities.

TypeScript support for Amplify Hub channels

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)

auth-hub-v5

Proposed DX (v6)

auth-hub-v6

TypeScript support for custom Hub channels

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;
  }
});

custom-hub-v5

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;
  }
});

custom-hub-v6

Related issue: Fully typed hubs (#5997)

TypeScript support for Amplify Configuration

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
});

configure-v5

Proposed Usage (v6)

const authConfig : AuthConfig = {
  userPoolId: 'us-east-1_0yqxxHm5q',
  userPoolClientId: '3keodiqtm52nhh2ls0vQfs5v1q',
  signUpVerificationMethod: 'code'
};

Amplify.configure({
  Auth: authConfig
});

configure-v6

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

Auth Category Changes

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.

TypeScript support for user attributes

User attributes inference on the signUp API.

Current Usage (v5)

Auth.signUp({
  username: 'username',
  password: '*******',
  attributes: {
    email: 'email@domain.com'
  }
});

signup-v5

Proposed Usage (v6)

Auth.signUp({
  username: "username",
  password: "*******",
  options: {
    userAttributes: {
      email: "email@domain.com",
    },
  },
});

user attributes

Related issue: TypeScript definition not matching: Property 'attributes' does not exist on type 'CognitoUser' (#9941)

Predictable API responses

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
}

confirmSignUp-v5

Proposed Usage (v6)

const resp = await confirmSignUp({
  username: 'username',
  confirmationCode: '112233',
});

if (resp.isSignUpComplete) {
  // Show login component
}

confirmSignUp-v6

Related issues:

Try out the proposed types here: https://stackblitz.com/edit/rfc-typescript-v6?file=examples-auth.ts

Storage Category Changes

Amplify is proposing the following changes for the Storage category.

Introduction of object reference types

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' }),
  });
}

Splitting up the get API

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 });

Changes to the put return object

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

GraphQL API Category Changes

Amplify is proposing the following changes for the GraphQL API category to improve type safety and readability.

Introduce dedicated, type-safe query(), mutation(), and subscription() GraphQL operation APIs

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})`),
});

Less verbose type definitions for generated queries, mutations, and subscriptions

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";

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';

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);
      },
    }
  });
}

Flatten GraphQL operation responses

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 behavior for single query/mutation in the response

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
}

Proposed behavior for multiple queries/mutations in the response

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);

Type safety for GraphQL query, mutation, subscription inputs

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`
});

Bug fix: Add __typename to GraphQL operations' selection set

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.

Bug fix: Remove any cast needed for subscriptions

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();

REST API Category Changes

Amplify is proposing the following changes for the REST API category.

First param is an object with named parameters

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: {},
  response: true,
  queryStringParameters: {
    name: "param",
  },
};

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",
    },
  }
);

Adding TypeScript generics to request body and response

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));

Type narrowing on runtime errors

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

@jimblanc jimblanc added the Feedback Feedback on our library label Mar 21, 2023
@Ashish-Nanda Ashish-Nanda pinned this issue Mar 21, 2023
@abdallahshaban557 abdallahshaban557 added the TypeScript Related to TypeScript issues label Mar 22, 2023
@olliethedev
Copy link

Awesome improvements here!

My specific feedback is for the GraphQL API Category Changes:

  1. For the proposed behavior for single query/mutation in the response, Option 1 (Flatten to the lowest level) seems more intuitive and straightforward. It would make accessing the returned data more efficient and minimize the complexity of handling the response.
  2. Regarding the proposed behavior for multiple queries/mutations in the response, Option 1 (Flatten to data level) seems to be the best choice, as it provides an easy way to access the data while maintaining a clear connection between the query/mutation and its result.

@hanna-becker
Copy link

hanna-becker commented Mar 23, 2023

Awesome! I'm excited about having types for the auth channel's event payload, after realizing a while ago that the payload of a "signIn" event differs depending on whether it originates from native Cognito or a social provider sign in.

@hanna-becker
Copy link

Awesome improvements here!

My specific feedback is for the GraphQL API Category Changes:

  1. For the proposed behavior for single query/mutation in the response, Option 1 (Flatten to the lowest level) seems more intuitive and straightforward. It would make accessing the returned data more efficient and minimize the complexity of handling the response.
  2. Regarding the proposed behavior for multiple queries/mutations in the response, Option 1 (Flatten to data level) seems to be the best choice, as it provides an easy way to access the data while maintaining a clear connection between the query/mutation and its result.

I wonder, though, what the flattened to the lowest level response would look like for an error case. Right now, there's a data object and an array of errors in the response in an error case. While I agree that Option 1 is more straight-forward for the happy case, I believe Option 2 would make error handling easier for a single query/mutation.

@dayhaysoos
Copy link

Looking great so far! Halfway through, but I have a question about the Custom Event Data types in the proposed experience. Are those custom types made manually or are they coming from Codegen somehow?

@dayhaysoos
Copy link

Thank you all for the feedback so far, much appreciated!

@dayhaysoos Are you referring to the custom event types for Hub? If so, these types would be defined by our customers based on the structure of their application's custom events (i.e. not codegen'd). Does this answer your question?

Yes, this answers my question. Thank you!

@kvramyasri7 kvramyasri7 unpinned this issue Apr 20, 2023
@jhechtf
Copy link

jhechtf commented Apr 22, 2023

This is more a question than anything, but it sits sort of in the same vein as this:

Are there any plans on including some way to get credentials directly through the refresh token? Using the Auth code manually in a react app (for some reason) still kept the user credentials. In trying to set up an Angular app I've run into the only place that has a way that I can see to refresh the session is on the CognitoUser (which honestly I would like to see some improvements to as well, but that's a separate topic).

It feels like there should be, and it is entirely possible I'm missing it since the docs for the library could be improved, but if we are going to talk about improving the types of the repo, this feels like something that should be added to it as well.

@ebisbe
Copy link

ebisbe commented Apr 25, 2023

Are errors thrown by the Auth package considered? I use them.

@jimblanc
Copy link
Contributor Author

@ebisbe That's correct, we'll be improving our type support for Auth errors emitted by the library.

@jhechtf Thank you for your feedback! It is currently possible to refresh the session via currentSession if that resolves your issue. If you're still having trouble I'd recommend opening a ticket for us to debug further.

@therealkh
Copy link

Not all types are importable. For example I need HubCallback type and it can't be imported from aws-amplify, only from @aws-amplify/core

@jhechtf
Copy link

jhechtf commented Jun 7, 2023

Seconding @therealkh, even if you can't think of a reason why someone downstream would need access to a type, please export it wherever possible.

@charlieforward9
Copy link

It would be helpful to have an easy to use type serving as the parent of a model:

Current Usage

async fetchModelByID(model: any, id: string): Promise<any> {
    try {
      return await DataStore.query(model, (m) => m.id.eq(id)); <-- Warns that m is of type any
    } catch (error) {
      ...
    }
  }

Proposed Usage

async fetchModelByID(model: ModelClass, id: string): Promise<ModelClass> {
    try {
      return await DataStore.query(model, (m) => m.id.eq(id)); <-- Would detect m being of type ModelClass
    } catch (error) {
      ...
    }
  }

For example, assume I have a user model, I could call:

const user = fetchModelByID(User, id) 

@BearCooder
Copy link

BearCooder commented Jun 21, 2023

Hey @jimblanc its great to see the TS improvements! I have some questions:

Will the provided example in JS also be available as TS example?

Currently it is not possible (without some janky hacking) to rewrite the example as same example in Typescript due to this issue: #7426 & #9968 and thats not great at all. Hope withSSRContext will get type definitions too.

Especially the SSR part of the example.

export async function getServerSideProps({ req }) {
  const SSR = withSSRContext({ req });

  try {
    const response = await SSR.API.graphql({
      query: listPosts,
      authMode: "API_KEY",

Is there any ETA for all the TS improvements?

@abdallahshaban557
Copy link
Contributor

Hi @BearCooder - all of our examples will have TS variations! On the hacky workarounds for enabling TypeScript, we are fully aware of those, and we will be resolving them in our next major version of the Amplify JavaScript library. We do not have an ETA at this time, but we will keep this RFC updated as we move forward!

@johnf
Copy link
Contributor

johnf commented Jun 26, 2023

Something I came across today. Based on the current docs, I believe that the correct way of currently using amplify is

// GOOD
import { Storage } from 'aws-amplify';
// BAD
import { Storage } from '@aws-amplify/storage'

This makes sense to me because it ensures that all the different sub-packages get kept in sync.

However, aws-amplify doesn't export any of the types. So I end up having to do this everywhere.

import { Auth, Hub } from 'aws-amplify';
import { CognitoHostedUIIdentityProvider, CognitoUser } from '@aws-amplify/auth';

This also means I either need to tell eslint not to complain about the second line or add @aws-amplify/auth to my package.json

So it would be good to have the useful types exported by aws-amplify.

@abdallahshaban557
Copy link
Contributor

Hi @johnf - thank you for raising that concern! We are working with our team to make sure that these types are available to you even when using the aws-amplify package as you have mentioned.

@IamCocoDev
Copy link

IamCocoDev commented Jul 20, 2023

Hey there, amazing AWS Amplify Team!

First of all, I want to express my gratitude for creating such an awesome framework like AWS Amplify. You all rock! 🎉

I've been enjoying using Amplify for my various projects, but I noticed a couple of areas where we can make it even better, especially for TypeScript users like me. So, I'd like to request the following enhancements that would bring joy and smiles to developers worldwide:

  1. TypeScript Improvements:
    It would be amazing if Amplify could provide TypeScript typings.

  2. Error Message Translation:
    For a better user experience, we often need to provide localized error messages. Currently, we have to hardcode translations inside our application, like this:

// Spanish translation of the error message
case "Password must have uppercase characters":
  SwalCustom(
    "¡Error!",
    "La contraseña debe contener al menos una letra mayúscula",
    "error"
  );
  break;

It would be super cool if Amplify could offer a built-in mechanism for error message translation. This way, we can provide smoother internationalization support without resorting to manual translations.

  1. Error Message Standardization:
    There are some variations in the error messages returned by Amplify, especially in Cognito errors. For instance, some messages say "did not conform to policy," while others go with "must have." Let's standardize these error messages to make life easier for developers and improve the overall user experience.

I believe implementing these enhancements would make AWS Amplify even more fantastic, and developers like me would be over the moon!

Thank you for considering my suggestions, and keep up the great work! 🚀

Cheers,
IamCocoDev

@abdallahshaban557
Copy link
Contributor

Hi @IamCocoDev - thank you for the positivity and encouragement - we appreciate that a lot! Regarding your mentioned points:

  1. Absolutely on improving our typing! That is exactly what this RFC is addressing!
  2. On internationalization, can you give us a quick walkthrough of what you would expect that experience to be in a new feature request on Github? This will help us better track interest in this from our community.
  3. As part of our next major version, we are also trying to make sure our error responses are standardized to make catching errors and displaying information to your customers easier.

@IamCocoDev
Copy link

Hola@IamCocoDev - gracias por la positividad y el aliento - ¡lo apreciamos mucho! Respecto a los puntos mencionados:

  1. ¡Absolutamente en mejorar nuestra mecanografía! ¡Eso es exactamente lo que aborda este RFC!
  2. En cuanto a la internacionalización, ¿puede darnos un breve recorrido por lo que esperaría que fuera esa experiencia en una solicitud de nueva función en Github? Esto nos ayudará a realizar un mejor seguimiento del interés de nuestra comunidad en esto.
  3. Como parte de nuestra próxima versión principal, también estamos tratando de asegurarnos de que nuestras respuestas a errores estén estandarizadas para facilitar la detección de errores y mostrar información a sus clientes.

Hi @IamCocoDev - thank you for the positivity and encouragement - we appreciate that a lot! Regarding your mentioned points:

  1. Absolutely on improving our typing! That is exactly what this RFC is addressing!
  2. On internationalization, can you give us a quick walkthrough of what you would expect that experience to be in a new feature request on Github? This will help us better track interest in this from our community.
  3. As part of our next major version, we are also trying to make sure our error responses are standardized to make catching errors and displaying information to your customers easier.

Hi AWS Amplify Team,

  1. TypeScript Improvements: It's great to know that you're working on improving TypeScript typings. This will undoubtedly make the development process smoother for TypeScript users like me. I look forward to seeing these enhancements in action!

  2. Error Message Standardization: I appreciate that you're striving to standardize error messages. Consistency in error messages is essential for developers, and it will indeed enhance the overall user experience. I can't wait to benefit from this improvement.

As for the topic of internationalization, I'd be happy to provide more details and a feature request on GitHub. Here's a brief overview of what I envision:

Error Message Translation: It would be wonderful if AWS Amplify could offer a built-in mechanism for error message translation. Ideally, this feature would allow developers to:

  • Define translations for various languages within their Amplify project.
  • Associate translations with specific error codes or messages.
  • Dynamically switch between translations based on the user's language preference.

This way, developers wouldn't need to hardcode translations manually, making it much easier to support international users. I believe this addition would be a game-changer for applications targeting a global audience.

Once again, thank you for considering these suggestions and for your dedication to making AWS Amplify even better. Your responsiveness and commitment to the developer community are truly commendable!

Looking forward to the continued evolution of AWS Amplify.

Best regards,
IamCocoDev

@samturner3
Copy link

(Another request for TS improvement - not sure if this is best place to post? - admin please move if not)

A few people have asked for type improvement for the Lambda function resolver, ie Structure of the function event;

I have solved it in my own code:

Solution
import {
  type AppSyncIdentity,
  type AppSyncResolverEventHeaders,
} from 'aws-lambda'

/**
 * See https://docs.amplify.aws/cli/graphql/custom-business-logic/#lambda-function-resolver
 *
 * @param TArguments type of the arguments
 * @param TSource type of the source
 */
export interface AmplifyResolverEvent<
  TArguments,
  TSource = Record<string, any> | null,
> {
  /**
   * typeName: The name of the parent object type of the field being resolved.
   */
  typeName: string
  /**
   * fieldName: The name of the field being resolved.
   */
  fieldName: string
  /**
   * arguments: A map containing the arguments passed to the field being resolved.
   */
  arguments: TArguments
  /**
   * identity: A map containing identity information for the request. Contains a nested key 'claims' that will contains the JWT claims if they exist.
   */
  identity?: AppSyncIdentity
  /**
   * source: When resolving a nested field in a query, the source contains parent value at runtime. For example when resolving Post.comments, the source will be the Post object.
   */
  source: TSource
  /**
   * request: 	The AppSync request object. Contains header information.
   */
  request: {
    headers: AppSyncResolverEventHeaders
    /** The API's custom domain if used for the request. */
    domainName: string | null
  }
  /**
   * prev: When using pipeline resolvers, this contains the object returned by the previous function. You can return the previous value for auditing use cases.
   */
  prev: { result: Record<string, any> } | null
}

I gathered this by looking at interface AppSyncResolverEvent in aws-lambda which speciflly mentions Maintainer's note: Some of these properties are shared with the Amplify resolver. It may be worth checking if changes here may be applicable there too.

And in the AppSync mapping template resolver we can see how this type is built:

Before mapping template:

## [Start] Stash resolver specific context.. **
$util.qr($ctx.stash.put("typeName", "Mutation"))
$util.qr($ctx.stash.put("fieldName", "executeSearch"))
{}
## [End] Stash resolver specific context.. **

Request Mapping Template:

## [Start] Invoke AWS Lambda data source: ExecuteSearchLambdaDataSource. **
{
  "version": "2018-05-29",
  "operation": "Invoke",
  "payload": {
      "typeName": $util.toJson($ctx.stash.get("typeName")),
      "fieldName": $util.toJson($ctx.stash.get("fieldName")),
      "arguments": $util.toJson($ctx.arguments),
      "identity": $util.toJson($ctx.identity),
      "source": $util.toJson($ctx.source),
      "request": $util.toJson($ctx.request),
      "prev": $util.toJson($ctx.prev)
  }
}
## [End] Invoke AWS Lambda data source: ExecuteSearchLambdaDataSource. **

Happy to open a PR (not sure where this would live) or leave here, not sure on best workflow.

@renebrandel
Copy link

Hi @samturner3 - we're tracking the backend part of "typescript" support. Your concerns are very valid and we'd love to share some of our thinking with you. LMK if you're interested to hop on a call to get a sneak peak. If so, email me at: renbran at amazon dot com

@cwomack
Copy link
Contributor

cwomack commented Nov 16, 2023

With the release of the latest major version of Amplify (aws-amplify@>6), we'll close this RFC. Please refer to our release announcement, migration guide, and documentation for more information.

@cwomack cwomack closed this as completed Nov 16, 2023
@cwomack cwomack unpinned this issue Nov 16, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Feedback Feedback on our library TypeScript Related to TypeScript issues
Projects
None yet
Development

No branches or pull requests