How to Generate TypeScript Types from a DynamoDB Table
In Postgres you'd introspect information_schema and generate types from it.
DynamoDB has no equivalent — and it's not a missing feature, it's the data
model: DynamoDB stores no item schema at all. The only attributes the
service knows about are the ones used in keys. DescribeTable's
AttributeDefinitions is explicit about its own scope: each entry "describes
one attribute in the table and index key schema"
(AWS API reference) —
your table's other fifty attributes simply aren't recorded anywhere.
So "generate TypeScript types from DynamoDB" always means one of three things: declare the shape yourself, derive it from a schema you author in code, or infer it from the items that actually exist.
How do I get TypeScript types for a DynamoDB table?
There's no API that returns a table's item shape — DescribeTable only knows
the key attributes. Your options: hand-write an interface and validate at
the boundary (a Zod schema makes the types and the runtime check one
artifact), use a schema-first library where the schema you author yields the
types, or infer the shape from real items — by script, or with a tool like
DynoTable that scans the table and exports a TypeScript
interface, Zod schema, or JSON Schema.
- Method 1: hand-write the interface (+ Zod at the boundary)
- Method 2: schema-first libraries (types from a schema you author)
- Method 3: infer from the data itself
Method 1: hand-write the interface + validate at the boundary
The AWS SDK can't type your items for you. The v3 Document client returns
items as untyped records — every GetCommand / QueryCommand result is
effectively Record<string, unknown> until you assert otherwise. A bare
as Order cast compiles fine and lies at runtime, which is why the robust
version pairs the interface with a runtime check:
import {z} from 'zod';
const Order = z.object({
PK: z.string(), // ORDER#<id>
SK: z.string(), // META
status: z.enum(['open', 'shipped', 'cancelled']),
total: z.number(),
couponCode: z.string().optional() // sparse attribute
});
type Order = z.infer<typeof Order>;
const {Item} = await doc.send(new GetCommand({TableName: 'Orders', Key: key}));
const order = Order.parse(Item); // typed AND verifiedOne schema, two jobs: z.infer gives you the static type, parse catches the
item that doesn't match it — which in a schemaless store is a when, not an
if. The catch is equally plain: the schema documents your intent, not
your table. Nothing stops an old writer from having stored total as a
string, and hand-written types drift silently as the data evolves.
If you're working from raw (non-Document-client) API output, remember the wire
shape is type-tagged DynamoDB-JSON ({"S": "..."}, {"N": "123"}) — see
marshalling, and use the
DynamoDB JSON converter to flip a sample
between wire and plain form while you write the schema.
Method 2: schema-first libraries
Toolkits like ElectroDB and DynamoDB-Toolbox attack the drift problem from the write side: you author an entity schema in code, and the library derives the TypeScript types and enforces the shape on every read and write it performs. That's the strongest guarantee available — but note the direction: you write the schema; the library doesn't discover it. Pointing one at an existing table still means reverse-engineering the item shapes yourself first, and items written outside the library are outside its guarantees. They shine on greenfield single-table designs where every entity goes through the toolkit from day one.
Method 3: infer the types from real items
For an existing table, the ground truth is the data. Scan a sample, union the shapes:
const seen = new Map<string, Set<string>>(); // attr -> observed types
let count = 0;
let key: Record<string, unknown> | undefined;
do {
const page = await doc.send(new ScanCommand({TableName: 'Orders', ExclusiveStartKey: key}));
for (const item of page.Items ?? []) {
count++;
for (const [attr, value] of Object.entries(item)) {
const t = Array.isArray(value) ? 'array' : typeof value;
(seen.get(attr) ?? seen.set(attr, new Set()).get(attr)!).add(t);
}
}
key = page.LastEvaluatedKey;
} while (key && count < 5000);
// emit: attribute -> type union, optional if seen in < count itemsReal-world pitfalls the naive version hits immediately:
- Sparse attributes. DynamoDB items in one table can have different
attributes; an attribute present on 80% of items is
optional, not missing. Track per-attribute frequency, not just presence. - Mixed entities. In a single-table design,
USER#andORDER#items share the table — one merged interface for both is useless. Partition the sample by the type attribute and emit one type per entity. - Type collisions. The same attribute stored as
Nhere andSthere is a real (and common) data bug — surface it as a union rather than silently picking one. The full tag set is in data types. - A sample is a sample. Attributes that only appear on rare items may not be in your first 5,000 — and the scan costs read capacity either way (query vs scan).
One-click inference in DynoTable
That inference script — sampling, frequency tracking, nested paths, the per-entity split — is built into DynoTable's Table stats panel:
- Open a table, hit the Stats button (the bar-chart icon), and click
Index table. DynoTable scans the table once with live progress and
discovers every attribute your items carry — including nested ones by
dotted path, like
commonData.status— with its type and whether it was required or optional across the rows scanned. - Click Export and pick a format:
- TypeScript — an
interface. - Zod — a
z.object(...)schema (Standard-Schema compatible). - JSON Schema — draft 2020-12.
- TypeScript — an
- Copy it to the clipboard or save it to a file.

The export is honest about what it is: every generated schema opens with a note that it was inferred from the sampled items — a strong starting point, not an authoritative contract. Optionality reflects how often each attribute appeared while indexing, and primary-key attributes are always marked required. Indexing incurs normal DynamoDB read costs, and Reindex refreshes the picture after your data changes.
FAQ
Can I generate types from DescribeTable?
Only for the key attributes. AttributeDefinitions covers the table and
index key schema — nothing else about your items is stored by the service,
so there is no server-side schema to introspect.
What's the best way to type an existing production table? Infer first, then harden: sample the real items (script or DynoTable's indexed export) to get the actual shape, review it, and promote it to a hand-owned Zod schema or a schema-first library entity so future drift is caught at the boundary.
How do I handle multiple entity types in one table? One type per entity, never one merged type. Split the sample on your type attribute (or key prefix) and generate a separate interface for each — the discriminated union of those is your table type.
Why do my generated types say a required field is optional? Because some sampled item didn't have it. In a schemaless store optionality is an observation, not a declaration — check whether those items are legacy rows to backfill (see migrations) or a genuinely optional attribute.
Do the types cover DynamoDB sets and binary? A converter has to choose plain-JSON representations: sets become arrays and binary becomes an encoded string — the same mapping quirks covered in marshalling. Round-trip a sample through the DynamoDB JSON converter to see exactly what your attributes look like on each side.
Stop guessing your table's shape — download DynoTable, index the table, and export a TypeScript, Zod, or JSON Schema in one click.


