AWSDynamoDB | 5 Min Read

Accurately Typing DynamoDB Data from the AWS SDK using TypeScript

Learn how to accurately and easily type data retrieved from a DynamoDB table via the AWS SDK using TypeScript and how to avoid using any types!

If you’ve ever tried building a project with DynamoDB and used the AWS SDK with TypeScript to read and write data to the database, you’ll probably be familiar with the issues of the SDK not typing the data you retrieve from the database.

This issue makes sense because the SDK has no way of knowing what attributes you have on every item in your database before you’ve retrieved them but this doesn’t make the issue any less annoying from a developer's perspective.

But, don’t worry in this tutorial, I’ll give you a solution so you can keep the power of the SDK and have accurate types from TypeScript for you to use in your application so let’s get into it.

The Issue

Before we jump into the solution let’s take a quick look at the problem, below is some example code using the `QueryCommand` from the AWS SDK to retrieve all items in the database that match the given `pk` and start with the same `sk` value.

1const { Items } = await dbClient.send(
2 new QueryCommand({
3 TableName: "YOUR_TABLE_NAME",
4 ExpressionAttributeValues: {
5 ":p": pk,
6 ":s": sk,
7 },
8 KeyConditionExpression: "pk = :p and begins_with(sk, :s)",
9 })
10);
ts

The issue with this code is the TypeScript type given to `Items` will be `Record<string, any>[] | undefined` which is far from helpful. If we wanted to access the properties on the items returned from our database, TypeScript would throw an error as it has no idea what is present on the data and what isn’t so let’s fix that so we can access the actual data we retrieve with the correct types.

The Solution

To resolve the issue with the types not being applied to the data being returned, we first need to do some digging into the SDK package itself. If we dig into the `QueryCommand` we used above, we can actually see the return type being used is the one below.

1export declare type QueryCommandOutput = Omit<
2 __QueryCommandOutput,
3 "Items" | "LastEvaluatedKey"
4> & {
5 Items?: Record<string, NativeAttributeValue>[];
6 LastEvaluatedKey?: Record<string, NativeAttributeValue>;
7};
ts

This is where we can see the not-very helpful `Record<string, NativeAttributeValue>[] | undefined` is being returned to us. But, what if we could override that with our own types?

What we’re going to do is take the `QueryCommandOutput` type and remove the built-in type for `Items` and instead give it our own type that matches the data we’re retrieving from the database. We can do this with the below code.

1import { QueryCommandOutput } from "@aws-sdk/lib-dynamodb";
2
3type Item = {
4 prop1: string;
5};
6
7const { Items } = (await dbClient.send(
8 new QueryCommand({
9 TableName: "YOUR_TABLE_NAME",
10 ExpressionAttributeValues: {
11 ":p": pk,
12 ":s": sk,
13 },
14 KeyConditionExpression: "pk = :p and begins_with(sk, :s)",
15 })
16)) as Omit<QueryCommandOutput, "Items"> & {
17 Items?: Item[];
18};
ts

As mentioned above, we take the `QueryCommandOutput` type and use it with the `Omit` feature from TypeScript to remove the `Items` type. We then extend that type with our own type for `Items` which will be used for the `Items` property returned from the SDK.

So, in the case of our example above we took our `Items` property from having a type of `Record<string, NativeAttributeValue>[] | undefined` to having a type of `Item[] | undefined` which is a lot nicer to use and work with!

Making It Reusable With Generics

But, let’s not stop there, we can take this one step further and make our solution easier to reuse across our entire project by pairing it with generics and creating a new standalone type like the one below.

1export type IQueryCommandOutput<T> = Omit<QueryCommandOutput, "Items"> & {
2 Items?: T;
3};
ts

We can now use this type with any `QueryCommand` in our codebase and provide it with a type to substitute with the generic `T` so we can have nicely typed data for all of our queries. Here is our example updated to use this new abstraction.

./types.ts
1import { QueryCommandOutput } from "@aws-sdk/lib-dynamodb";
2
3export type Item = {
4 prop1: string,
5};
6
7export type IQueryCommandOutput<T> = Omit<QueryCommandOutput, "Items"> & {
8 Items?: T,
9};
ts
./query.ts
1import { Item, IQueryCommandOutput } from "./types";
2
3const { Items } = (await dbClient.send(
4 new QueryCommand({
5 TableName: "YOUR_TABLE_NAME",
6 ExpressionAttributeValues: {
7 ":p": pk,
8 ":s": sk,
9 },
10 KeyConditionExpression: "pk = :p and begins_with(sk, :s)",
11 })
12)) as IQueryCommandOutput<Item[]>;
ts

Example Code Snippets

We’ve covered the `QueryCommand` from the AWS SDK throughout this post but here are the snippets you’ll need for the other common commands used when interacting with a DynamoDB table via the AWS SDK.

Scan

1export type IScanCommandOutput<T> = Omit<ScanCommandOutput, "Items"> & {
2 Items?: T;
3};
ts

Query

1export type IQueryCommandOutput<T> = Omit<QueryCommandOutput, "Items"> & {
2 Items?: T;
3};
ts

Get

1export type IGetCommandOutput<T> = Omit<GetCommandOutput, "Item"> & {
2 Item?: T;
3};
ts

Put

1export type IPutCommandOutput<T> = Omit<PutCommandOutput, "Attributes"> & {
2 Attributes?: T;
3};
ts

Delete

1export type IDeleteCommandOutput<T> = Omit<
2 DeleteCommandOutput,
3 "Attributes"
4> & {
5 Attributes?: T;
6};
ts

Update

1export type IUpdateCommandOutput<T> = Omit<
2 UpdateCommandOutput,
3 "Attributes"
4> & {
5 Attributes?: T;
6};
ts

Considerations

While this method does work great and all of the values returned to you from the SDK will have the appropriate types you passed to the generic, something you will need to bare in mind is that you’ll need to manually maintain the TypeScript types/interfaces you use.

This is because the types can’t be created from the database items themselves so we need to manually create the types and align them with the data in the database. This of course could introduce some issues if the types aren’t kept up to date or aren’t accurate to the data being stored.

However, I do think this issue is a minor one compared to the benefits of having working types assigned to the data returned from the SDK.

Closing Thoughts

We’ve now reached the end of this post; in this tutorial, we’ve looked at how to type the data returned to us from a DynamoDB table using the AWS SDK in a TypeScript project. I hope you found this post helpful until next time.

Thank you for reading

Coner



Content

Latest Blog Posts

Below is my latest blog post and a link to all of my posts.

View All Posts

Content

Latest Video

Here is my YouTube Channel and latest video for your enjoyment.

View All Videos
Set Up Next.js (App Router) Authentication in Minutes with Clerk!

Set Up Next.js (App Router) Authentication in Minutes with Clerk!

Contact

Join My Newsletter

Subscribe to my weekly newsletter by filling in the form.

Get my latest content every week and 0 spam!