中級読了 2 分

DynamoDBテーブルからTypeScriptの型を生成する方法

Postgresならinformation_schemaをイントロスペクトして、そこから型を生成するでしょう。DynamoDBに相当するものはありません — そしてそれは欠けている機能ではなく、データモデルそのものです:DynamoDBはアイテムのスキーマを一切保存しません。 サービスが知っている属性は、キーに使われているものだけです。DescribeTableAttributeDefinitionsは、自身のスコープについて明示的です:各エントリは「テーブルおよびインデックスのキースキーマ内の1つの属性を記述します」 (AWS APIリファレンス) — テーブルの残り50個の属性は、単にどこにも記録されていないのです。

したがって「DynamoDBからTypeScriptの型を生成する」は、常に次の3つのいずれかを意味します:形を自分で宣言する、コードで書いたスキーマから導出する、あるいは実際に存在するアイテムから推論する。

DynamoDBテーブルのTypeScript型を得るには?

テーブルのアイテムの形を返すAPIはありません — DescribeTableはキー属性しか知りません。選択肢は:interfaceを手書きして境界でバリデーションする(Zodスキーマなら型とランタイムチェックが1つの成果物になります)、書いたスキーマから型が得られるスキーマファーストのライブラリを使う、あるいは実際のアイテムから形を推論する — スクリプトで、またはDynoTableのようにテーブルをスキャンしてTypeScriptのinterface、Zodスキーマ、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

1つのスキーマで2つの仕事です:z.inferが静的な型を与え、parseがそれに一致しないアイテムを捕まえます — スキーマレスなストアでは、それは「もし」ではなく「いつ」の問題です。難点も同じくらい明白です:スキーマが文書化するのはあなたの意図であって、テーブルではありません。古いライタがtotalを文字列として保存していたことを妨げるものは何もなく、手書きの型はデータの進化とともに静かにずれていきます。

生の(Documentクライアントを介さない)API出力を扱うなら、ワイヤ上の形が型タグ付きのDynamoDB-JSON({"S": "..."}{"N": "123"})であることを忘れないでください — マーシャリングを参照し、スキーマを書く間はDynamoDB JSONコンバータでサンプルをワイヤ形式とプレーン形式の間で行き来させてください。

方法2:スキーマファーストのライブラリ

ElectroDBDynamoDB-Toolboxのようなツールキットは、ドリフトの問題を書き込み側から攻めます:コードでエンティティスキーマを書くと、ライブラリがTypeScriptの型を導出し、_かつ_自身が実行するすべての読み書きでその形を強制します。これは手に入る中で最も強い保証です — ただし方向に注意してください:スキーマを書くのはあなたであって、ライブラリが発見してくれるわけではありません。 既存のテーブルに向けるなら、やはりまずアイテムの形を自分でリバースエンジニアリングする必要があり、ライブラリの外で書かれたアイテムはその保証の外にあります。これらが輝くのは、初日からすべてのエンティティがツールキットを通る、グリーンフィールドのシングルテーブル設計です。

方法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

素朴なバージョンがすぐにぶつかる、現実世界の落とし穴:

  • スパース属性。 1つのテーブル内のDynamoDBアイテムは異なる属性を持てます。アイテムの80%に存在する属性はoptionalであって、欠落ではありません。存在の有無だけでなく、属性ごとの頻度を追跡してください。
  • エンティティの混在。 シングルテーブル設計では、USER#ORDER#のアイテムがテーブルを共有します — 両方をマージした1つのinterfaceは役に立ちません。型属性でサンプルを分割し、エンティティごとに1つの型を出力してください。
  • 型の衝突。 同じ属性がこちらではN、あちらではSとして保存されているのは、実在する(そしてよくある)データのバグです — 黙って片方を選ぶのではなく、ユニオンとして表面化させてください。タグの全セットはデータ型にあります。
  • サンプルはサンプルです。 まれなアイテムにしか現れない属性は、最初の5,000件には含まれないかもしれません — そしてどちらにせよスキャンには読み取りキャパシティのコストがかかります(query vs scan)。

DynoTableでのワンクリック推論

この推論スクリプト — サンプリング、頻度の追跡、ネストされたパス、エンティティごとの分割 — は、DynoTableTable statsパネルに組み込まれています:

  1. テーブルを開き、Statsボタン(棒グラフのアイコン)を押して、Index tableをクリックします。DynoTableはライブの進捗表示付きでテーブルを1回スキャンし、アイテムが持つすべての属性 — commonData.statusのようにドット区切りパスで表されるネストされたものを含む — を、その型と、スキャンした行全体で必須かオプションかとともに発見します。
  2. Exportをクリックしてフォーマットを選びます:
    • TypeScriptinterface
    • Zodz.object(...)スキーマ(Standard-Schema互換)。
    • JSON Schema — draft 2020-12。
  3. クリップボードにコピーするか、ファイルに保存します。
DynoTableのTable statsパネル:型と必須/オプションのフラグ付きのインデックス済みフィールドのリストと、スキーマのExportボタン。
DynoTableのTable statsパネル:型と必須/オプションのフラグ付きのインデックス済みフィールドのリストと、スキーマのExportボタン。

このエクスポートは、自分が何であるかについて正直です:生成されたすべてのスキーマは、サンプリングされたアイテムから推論されたものであるという注記で始まります — 強力な出発点であって、権威ある契約ではありません。オプションかどうかはインデックス作成中に各属性が現れた頻度を反映し、プライマリキーの属性は常に必須としてマークされます。インデックス作成には通常のDynamoDB読み取りコストがかかり、データが変わった後はReindexで状況を更新できます。

FAQ

DescribeTableから型を生成できますか? キー属性についてだけです。AttributeDefinitionsがカバーするのはテーブルとインデックスのキースキーマだけで、それ以外のアイテムに関する情報はサービスに保存されていません。イントロスペクトできるサーバー側のスキーマは存在しないのです。

既存の本番テーブルに型を付ける最善の方法は? まず推論し、それから固めます:実際のアイテムをサンプリングして(スクリプトかDynoTableのインデックス済みエクスポートで)実際の形を取得し、レビューし、自分で管理するZodスキーマかスキーマファーストのライブラリのエンティティに昇格させます。そうすれば将来のドリフトは境界で捕まります。

1つのテーブルに複数のエンティティ型がある場合は? エンティティごとに1つの型で、マージした1つの型には決してしません。型属性(またはキーのプレフィックス)でサンプルを分割し、それぞれに個別のinterfaceを生成してください — それらの判別可能なユニオンがテーブルの型です。

生成された型で必須のはずのフィールドがオプションになっているのはなぜですか? サンプリングされたどれかのアイテムがそれを持っていなかったからです。スキーマレスなストアでは、オプションかどうかは宣言ではなく観察です — そのアイテムがバックフィルすべきレガシー行なのか(マイグレーションを参照)、本当にオプションな属性なのかを確認してください。

型はDynamoDBのセットやバイナリもカバーしますか? コンバータはプレーンJSONでの表現を選ばなければなりません:セットは配列に、バイナリはエンコードされた文字列になります — マーシャリングで扱っているのと同じマッピングの癖です。サンプルをDynamoDB JSONコンバータでラウンドトリップさせて、属性がそれぞれの側でどう見えるかを正確に確認してください。

テーブルの形を推測するのはやめましょう — DynoTableをダウンロードして、テーブルをインデックスし、TypeScript、Zod、またはJSON Schemaをワンクリックでエクスポートしてください。

更新日