Intermédiaire8 min de lecture

Comment générer des types TypeScript depuis une table DynamoDB

Dans Postgres, tu introspecterais information_schema et générerais les types à partir de là. DynamoDB n'a pas d'équivalent — et ce n'est pas une fonctionnalité manquante, c'est le modèle de données : DynamoDB ne stocke aucun schéma d'item. Les seuls attributs que le service connaît sont ceux utilisés dans les clés. Les AttributeDefinitions de DescribeTable sont explicites sur leur propre périmètre : chaque entrée « décrit un attribut du schéma de clés de la table et des index » (référence de l'API AWS) — les cinquante autres attributs de ta table ne sont tout simplement enregistrés nulle part.

Donc « générer des types TypeScript depuis DynamoDB » signifie toujours l'une de trois choses : déclarer la forme toi-même, la dériver d'un schema que tu écris en code, ou l'inférer des items qui existent réellement.

Comment obtenir des types TypeScript pour une table DynamoDB ?

Il n'y a pas d'API qui renvoie la forme des items d'une table — DescribeTable ne connaît que les attributs de clé. Tes options : écrire une interface à la main et valider à la frontière (un schema Zod fait des types et de la vérification runtime un seul artefact), utiliser une bibliothèque schema-first où le schema que tu écris produit les types, ou inférer la forme depuis de vrais items — par script, ou avec un outil comme DynoTable qui scanne la table et exporte une interface TypeScript, un schema Zod ou un JSON Schema.

Méthode 1 : écrire l'interface à la main + valider à la frontière

Le SDK AWS ne peut pas typer tes items pour toi. Le Document client v3 renvoie les items comme des enregistrements non typés — chaque résultat de GetCommand / QueryCommand est effectivement un Record<string, unknown> jusqu'à ce que tu affirmes le contraire. Un simple cast as Order compile très bien et ment au runtime, c'est pourquoi la version robuste associe l'interface à une vérification runtime :

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

Un schema, deux emplois : z.infer te donne le type statique, parse attrape l'item qui ne lui correspond pas — ce qui, dans un store sans schéma, est un quand, pas un si. Le piège est tout aussi évident : le schema documente ton intention, pas ta table. Rien n'empêche un ancien writer d'avoir stocké total comme une chaîne, et les types écrits à la main dérivent silencieusement à mesure que les données évoluent.

Si tu travailles depuis la sortie brute de l'API (sans Document client), rappelle-toi que le format réseau est du DynamoDB-JSON à tags de type ({"S": "..."}, {"N": "123"}) — voir le marshalling, et utilise le convertisseur DynamoDB JSON pour faire basculer un échantillon entre la forme réseau et la forme simple pendant que tu écris le schema.

Méthode 2 : les bibliothèques schema-first

Des toolkits comme ElectroDB et DynamoDB-Toolbox attaquent le problème de la dérive côté écriture : tu écris un schema d'entité en code, et la bibliothèque dérive les types TypeScript et impose la forme à chaque lecture et écriture qu'elle effectue. C'est la garantie la plus forte disponible — mais note la direction : c'est toi qui écris le schema ; la bibliothèque ne le découvre pas. En pointer une sur une table existante signifie toujours rétro-ingénier les formes d'items toi-même d'abord, et les items écrits hors de la bibliothèque sont hors de ses garanties. Elles brillent sur les single-table designs greenfield où chaque entité passe par le toolkit dès le premier jour.

Méthode 3 : inférer les types depuis de vrais items

Pour une table existante, la vérité terrain, ce sont les données. Scanne un échantillon, fais l'union des formes :

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

Les pièges du monde réel que la version naïve rencontre immédiatement :

  • Les attributs épars. Les items DynamoDB d'une même table peuvent avoir des attributs différents ; un attribut présent sur 80 % des items est optional, pas manquant. Suis la fréquence par attribut, pas juste la présence.
  • Les entités mélangées. Dans un single-table design, les items USER# et ORDER# partagent la table — une interface fusionnée pour les deux est inutile. Partitionne l'échantillon par l'attribut de type et émets un type par entité.
  • Les collisions de types. Le même attribut stocké en N ici et en S là est un vrai bug de données (et un bug courant) — fais-le remonter comme une union plutôt que d'en choisir un silencieusement. Le jeu complet de tags est dans les types de données.
  • Un échantillon n'est qu'un échantillon. Les attributs qui n'apparaissent que sur des items rares peuvent ne pas être dans tes 5 000 premiers — et le scan coûte de la capacité de lecture dans tous les cas (query vs scan).

L'inférence en un clic dans DynoTable

Ce script d'inférence — échantillonnage, suivi de fréquence, chemins imbriqués, la séparation par entité — est intégré au panneau Table stats de DynoTable :

  1. Ouvre une table, clique sur le bouton Stats (l'icône de graphique en barres), puis sur Index table. DynoTable scanne la table une fois avec la progression en direct et découvre chaque attribut que tes items portent — y compris les attributs imbriqués par chemin pointé, comme commonData.status — avec son type et s'il était requis ou optionnel sur l'ensemble des lignes scannées.
  2. Clique sur Export et choisis un format :
    • TypeScript — une interface.
    • Zod — un schema z.object(...) (compatible Standard-Schema).
    • JSON Schema — draft 2020-12.
  3. Copie-le dans le presse-papiers ou enregistre-le dans un fichier.
Le panneau Table stats de DynoTable : la liste des champs indexés avec types et indicateurs requis/optionnel, et le bouton Export de schéma.
Le panneau Table stats de DynoTable : la liste des champs indexés avec types et indicateurs requis/optionnel, et le bouton Export de schéma.

L'export est honnête sur ce qu'il est : chaque schema généré s'ouvre sur une note indiquant qu'il a été inféré des items échantillonnés — un point de départ solide, pas un contrat faisant autorité. L'optionalité reflète la fréquence d'apparition de chaque attribut pendant l'indexation, et les attributs de clé primaire sont toujours marqués requis. L'indexation engendre les coûts de lecture DynamoDB normaux, et Reindex rafraîchit le tableau après que tes données ont changé.

FAQ

Puis-je générer des types depuis DescribeTable ? Seulement pour les attributs de clé. AttributeDefinitions couvre le schéma de clés de la table et des index — rien d'autre sur tes items n'est stocké par le service, donc il n'y a pas de schéma côté serveur à introspecter.

Quelle est la meilleure façon de typer une table de production existante ? Inférer d'abord, durcir ensuite : échantillonne les vrais items (par script ou via l'export indexé de DynoTable) pour obtenir la forme réelle, revois-la, et promeus-la en schema Zod que tu possèdes ou en entité de bibliothèque schema-first pour que la dérive future soit attrapée à la frontière.

Comment gérer plusieurs types d'entités dans une même table ? Un type par entité, jamais un type fusionné. Sépare l'échantillon sur ton attribut de type (ou le préfixe de clé) et génère une interface distincte pour chacun — l'union discriminée de celles-ci est le type de ta table.

Pourquoi mes types générés disent-ils qu'un champ requis est optionnel ? Parce qu'un item échantillonné ne l'avait pas. Dans un store sans schéma, l'optionalité est une observation, pas une déclaration — vérifie si ces items sont des lignes legacy à rétro-remplir (voir les migrations) ou un attribut authentiquement optionnel.

Les types couvrent-ils les ensembles et le binaire de DynamoDB ? Un convertisseur doit choisir des représentations JSON simples : les ensembles deviennent des tableaux et le binaire devient une chaîne encodée — les mêmes particularités de conversion couvertes dans le marshalling. Fais faire l'aller-retour à un échantillon dans le convertisseur DynamoDB JSON pour voir exactement à quoi ressemblent tes attributs de chaque côté.

Arrête de deviner la forme de ta table — télécharge DynoTable, indexe la table, et exporte un TypeScript, Zod ou JSON Schema en un clic.

Mis à jour