Generate TypeScript Types from DynamoDB
In Postgres you'd introspect information_schema and generate types from it.
DynamoDB has no equivalent: 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 a strict
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 dialog:
- Open a table, hit the Settings button in the tab toolbar, pick
Indexing, and click Index table. DynoTable samples the table with live progress and records
the attributes it finds — including nested ones by dotted path, like
commonData.status— with each one's type and whether it was required or optional across the rows scanned. The scan is capped, so an attribute that only appears in rare items can be missing; see Table overview and indexing. - 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.

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.


