进阶阅读约 3 分钟

如何从 DynamoDB 表生成 TypeScript 类型

在 Postgres 里,你会内省 information_schema 并据此生成类型。DynamoDB 没有对应物——而且这不是缺失的功能,这就是它的数据模型:DynamoDB 完全不存储项的 schema。服务唯一知道的属性,是那些用在键里的属性。DescribeTableAttributeDefinitions 对自己的范围说得很明白:每个条目"描述表和索引键 schema 中的一个属性"(AWS API 参考)——你表里其余的五十个属性根本没有被记录在任何地方。

所以"从 DynamoDB 生成 TypeScript 类型"永远意味着三件事之一:自己声明形状,从你在代码里编写的 schema 推导它,或者从实际存在的项中推断它。

如何为一张 DynamoDB 表拿到 TypeScript 类型?

没有任何 API 会返回一张表的项形状——DescribeTable 只知道键属性。你的选项:手写一个 interface 并在边界处校验(一个 Zod schema 让类型和运行时检查合为一件工件)、用一个 schema 优先的库(你编写的 schema 产出类型),或者从真实的项推断形状——用脚本,或者用像 DynoTable 这样能扫描表并导出 TypeScript interface、Zod schema 或 JSON Schema 的工具。

方法 1:手写 interface + 在边界处校验

AWS SDK 无法替你给项定型。v3 Document 客户端返回的是无类型的记录——每个 GetCommand / QueryCommand 的结果实际上都是 Record<string, unknown>,直到_你_另行断言。一个光秃秃的 as Order 断言能顺利编译,却在运行时撒谎,这正是稳健版本把 interface 与运行时检查配对的原因:

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 verified

一份 schema,两份工作:z.infer 给你静态类型,parse 抓住那个与之不匹配的项——在一个无 schema 的存储里,这是_迟早_的事,而不是_万一_。缺点同样直白:schema 记录的是你的意图,不是你的表。没有什么能阻止某个老的写入方曾把 total 存成字符串,而手写的类型会随着数据演化悄悄漂移。

如果你处理的是原始(非 Document 客户端)API 输出,记住线上格式是带类型标签的 DynamoDB-JSON({"S": "..."}{"N": "123"})——参见 marshalling,并在编写 schema 时用 DynamoDB JSON 转换器把一份样本在线上格式和纯 JSON 之间来回翻转。

方法 2:schema 优先的库

ElectroDBDynamoDB-Toolbox 这样的工具包,从写入侧攻击漂移问题:你在代码里编写实体 schema,库据此推导 TypeScript 类型,_并_在它执行的每次读写上强制这个形状。这是能拿到的最强保证——但注意方向:schema 是你写的;库不会去发现它。把它指向一张既有的表,仍意味着先由你自己逆向还原项的形状,而绕过库写入的项不在它的保证范围内。它们在全新的单表设计上大放异彩——每个实体从第一天起就走这套工具包。

方法 3:从真实的项推断类型

对一张既有的表,真相就在数据里。扫描一份样本,把形状做并集:

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 items

朴素版本立刻会撞上的现实陷阱:

  • 稀疏属性。同一张表里的 DynamoDB 项可以有不同的属性;一个出现在 80% 项上的属性是 optional,不是不存在。要跟踪逐属性的出现频率,而不只是有没有。
  • 混合实体。单表设计里,USER#ORDER# 项共享一张表——给两者合并出一个 interface 毫无用处。按类型属性切分样本,为每种实体产出一个类型。
  • 类型冲突。同一个属性这里存成 N、那里存成 S,是真实(且常见)的数据 bug——把它作为联合类型暴露出来,而不是悄悄挑一个。完整的标签集在数据类型
  • 样本终归是样本。只出现在罕见项上的属性可能不在你的前 5,000 个里——而且无论如何这次扫描都要花读取容量(Query 与 Scan)。

DynoTable 中的一键推断

那个推断脚本——抽样、频率跟踪、嵌套路径、按实体切分——已经内置在 DynoTable表统计面板里:

  1. 打开一张表,点 Stats(统计)按钮(柱状图图标),再点 Index table(索引表)。DynoTable 对表做一次带实时进度的扫描,发现你的项携带的每一个属性——包括按点路径表示的嵌套属性,如 commonData.status——连同它的类型,以及在扫描过的行中它是必需还是可选
  2. Export(导出)并选一种格式:
    • TypeScript——一个 interface
    • Zod——一个 z.object(...) schema(兼容 Standard Schema)。
    • JSON Schema——draft 2020-12。
  3. 复制到剪贴板,或保存为文件。
DynoTable 的表统计面板:带类型和必需/可选标记的已索引字段列表,以及 schema 导出按钮。
DynoTable 的表统计面板:带类型和必需/可选标记的已索引字段列表,以及 schema 导出按钮。

这个导出对自己的身份很诚实:每份生成的 schema 开头都有一条注释,说明它是从被抽样的项推断而来——一个很强的起点,而不是权威契约。可选性反映的是索引期间各属性出现的频率,主键属性始终标记为必需。索引会产生正常的 DynamoDB 读取费用,而 Reindex(重建索引)会在数据变化后刷新这幅图景。

常见问题

能从 DescribeTable 生成类型吗? 只能覆盖键属性。AttributeDefinitions 只涵盖表和索引的键 schema——关于你的项,服务没有存储任何其他信息,所以不存在可内省的服务端 schema。

给一张既有生产表定型的最佳方式是什么? 先推断,再固化:对真实的项抽样(脚本或 DynoTable 的索引导出)拿到实际形状,审阅它,然后把它升格为一份由你掌管的 Zod schema 或某个 schema 优先库的实体,让未来的漂移在边界处被抓住。

同一张表里有多种实体类型怎么办? 每种实体一个类型,绝不合并成一个。按你的类型属性(或键前缀)切分样本,为每种实体生成单独的 interface——它们的可辨识联合(discriminated union)就是你的表类型。

为什么生成的类型把一个必填字段标成了可选? 因为某个被抽样的项没有它。在无 schema 的存储里,可选性是一种观察,不是一种声明——检查那些项是需要回填的历史行(见迁移),还是一个真正可选的属性。

这些类型覆盖 DynamoDB 的 set 和二进制吗? 转换器必须选择纯 JSON 表示:set 变成数组,二进制变成编码后的字符串——与 marshalling 里讲的映射怪癖相同。把一份样本在 DynamoDB JSON 转换器里走一个来回,就能看到你的属性在两侧各长什么样。

别再猜你表的形状了——下载 DynoTable,为表建索引,一键导出 TypeScript、Zod 或 JSON Schema。

更新于