DynamoDB
TypeScript
AWS
programming
troubleshooting

DynamoDB get item TypeScript hell

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

Most “TypeScript hell” around DynamoDB GetItem comes from using the low-level DynamoDB client directly. At that level, keys and items are expressed as DynamoDB AttributeValue maps such as {"S": "abc"}, which is correct but verbose, awkward, and easy to misuse in normal TypeScript code.

For most application code, the cleaner answer is to use DynamoDBDocumentClient from @aws-sdk/lib-dynamodb. It lets you work with normal JavaScript objects while still using the AWS SDK v3 command model.

Low-Level Client Versus Document Client

With the low-level client, a key must be wrapped in DynamoDB attribute types.

typescript
1import { DynamoDBClient, GetItemCommand } from "@aws-sdk/client-dynamodb";
2
3const client = new DynamoDBClient({ region: "us-east-1" });
4
5async function getRawUser() {
6  const result = await client.send(
7    new GetItemCommand({
8      TableName: "Users",
9      Key: {
10        userId: { S: "u-123" },
11      },
12    })
13  );
14
15  return result.Item;
16}

That is valid, but it exposes DynamoDB’s wire shape everywhere in your codebase.

With the document client, the same request is much easier to read.

typescript
1import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
2import { DynamoDBDocumentClient, GetCommand } from "@aws-sdk/lib-dynamodb";
3
4const baseClient = new DynamoDBClient({ region: "us-east-1" });
5const docClient = DynamoDBDocumentClient.from(baseClient);
6
7type User = {
8  userId: string;
9  email: string;
10  isActive: boolean;
11};
12
13async function getUser(userId: string): Promise<User | undefined> {
14  const result = await docClient.send(
15    new GetCommand({
16      TableName: "Users",
17      Key: { userId },
18    })
19  );
20
21  return result.Item as User | undefined;
22}

This is the main quality-of-life improvement. You work with plain JavaScript values instead of manual attribute wrappers.

Handle the Type Boundary Deliberately

Even with the document client, the SDK cannot know your domain model automatically. result.Item is still data coming from DynamoDB, not a proven User instance.

A pragmatic pattern is to keep the unsafe cast or validation in one place at the boundary.

typescript
1function assertUser(item: unknown): User {
2  const candidate = item as Partial<User> | undefined;
3  if (!candidate || typeof candidate.userId !== "string" || typeof candidate.email !== "string") {
4    throw new Error("Unexpected User item shape");
5  }
6
7  return {
8    userId: candidate.userId,
9    email: candidate.email,
10    isActive: Boolean(candidate.isActive),
11  };
12}

Then your application code can work with one typed helper instead of re-casting every call site.

Match the Key Schema Exactly

GetItem requires the full primary key. If the table uses both a partition key and a sort key, you must supply both.

typescript
1const result = await docClient.send(
2  new GetCommand({
3    TableName: "Orders",
4    Key: {
5      customerId: "c-1",
6      orderId: "o-99",
7    },
8  })
9);

If you provide only one part of a composite key, the request is invalid. A lot of “TypeScript hell” is really a key-schema mismatch hidden beneath generic error messages.

A Useful v3 Detail: Undefined Marshalling

In AWS SDK for JavaScript v3, the document client does not automatically drop undefined values the way many v2 users expect. If your input objects may contain undefined, configure marshalling explicitly.

typescript
1const docClient = DynamoDBDocumentClient.from(baseClient, {
2  marshallOptions: {
3    removeUndefinedValues: true,
4  },
5});

That matters more for writes than for GetCommand, but it is part of why teams feel the migration pain when mixing old examples with current v3 behavior.

Common Pitfalls

A common mistake is reaching for the low-level DynamoDB client when the document client is the real fit for application code. The low-level client is useful, but it should be the exception rather than the default.

Another issue is pretending result.Item is always present. GetItem may return no item at all, so undefined handling belongs in the function contract.

Developers also sometimes cast immediately to a domain type and skip all validation. That silences TypeScript but does not protect you from malformed or incomplete data.

Finally, make sure you do not mix low-level AttributeValue shapes and document-client shapes in the same request path. Once you choose one level of abstraction, stay consistent.

Summary

  • Most DynamoDB TypeScript pain comes from using the low-level client shape directly.
  • For normal app code, prefer DynamoDBDocumentClient with GetCommand.
  • Treat result.Item as optional and validate or narrow it at the boundary.
  • Supply the full key schema for tables with composite primary keys.
  • In SDK v3, configure marshalling intentionally if undefined values matter in your codebase.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.