---
title: "verbatra types"
description: "Erzeuge TypeScript-Deklarationen für deine Katalog-Keys und deren Nachrichtenargumente, ohne einen Provider aufzurufen oder einen API-Key zu lesen."
type: "reference"
---
# verbatra types

Erzeuge TypeScript-Deklarationen für deine Katalog-Keys und deren Nachrichtenargumente, ohne einen Provider aufzurufen oder einen API-Key zu lesen.

<AvailableFrom version="0.11.0" />

Mach aus einem vertippten Key oder einem vergessenen Interpolationsargument einen Compile-Fehler. `types` liest deinen Quellkatalog über den konfigurierten [Format-Adapter](https://verbatra.kreitz-webdev.de/de/docs/formats.md) und schreibt eine Deklarationsdatei: eine Union aller Keys und, pro Key, die Argumente, die dessen Nachricht nimmt. Der Befehl ruft keinen Provider auf, liest keinen API-Key und fasst nie eine Ziel-Locale-Datei an.

## Synopsis [#synopsis]

```bash
verbatra types [flags]
```

## Beispiele [#beispiele]

```bash
# write verbatra-types.d.ts from the source catalog
verbatra types

# write it where the app already imports from
verbatra types --out src/generated/messages.d.ts

# CI gate: exit 1 if the committed declaration is stale
verbatra types --check

# machine-readable result for a script
verbatra types --json
```

## Flags [#flags]

| Flag | Argument | Standard | Wirkung |
| --- | --- | --- | --- |
| `--out` | `<path>` | `verbatra-types.d.ts` | wohin die Deklaration geschrieben wird, relativ zum Arbeitsverzeichnis und innerhalb der Projektwurzel; fehlende Verzeichnisse werden angelegt |
| `--check` | keins | aus | die committete Deklaration Byte für Byte mit einer frischen Generierung vergleichen, ohne etwas zu schreiben; Exit `1`, wenn sie veraltet ist oder fehlt |

Außerdem nimmt der Befehl die [globalen Flags](https://verbatra.kreitz-webdev.de/de/docs/cli.md#globale-flags) `--cwd`, `--config` und `--json`.

## Verhalten [#verhalten]

### Was erzeugt wird [#was-erzeugt-wird]

Aus einer `locales/en.json` wie dieser:

```json
{
  "app": { "title": "Verbatra" },
  "greeting": "Hello {{name}}",
  "cart": { "item_one": "{{count}} item", "item_other": "{{count}} items" }
}
```

wird das hier:

```ts
// Generated by verbatra from locales/en.json (i18next-json). Do not edit by hand.
// Re-run `verbatra types` after the source catalog changes.

/** An argument whose type the source catalog does not record. */
export type VerbatraArgument = string | number;

/** The arguments of a message that takes none: passing any argument is a type error. */
export type VerbatraNoArguments = Record<string, never>;

/**
 * The arguments of a message verbatra could not analyse, so nothing is claimed about them.
 * Every key carrying this type is listed by `verbatra types` when it generates this file.
 */
export type VerbatraUnknownArguments = Record<string, VerbatraArgument>;

/** Every key in the source catalog, mapped to the arguments its message takes. */
export interface VerbatraMessages {
  "app.title": VerbatraNoArguments;
  "greeting": { readonly "name": VerbatraArgument };
  "cart.item": { readonly "count": VerbatraArgument };
  "cart.item_one": { readonly "count": VerbatraArgument };
  "cart.item_other": { readonly "count": VerbatraArgument };
}

/** Every key in the source catalog. */
export type VerbatraMessageKey = keyof VerbatraMessages;

/** The arguments the message at `Key` takes. */
export type VerbatraMessageArguments<Key extends VerbatraMessageKey> =
  VerbatraMessages[Key];

/** The keys the source catalog marks as carrying plural forms. */
export type VerbatraPluralMessageKey = "cart.item_one" | "cart.item_other";
```

Typisiere deine eigene Übersetzungsfunktion dagegen, den Rest erledigt der Compiler:

```ts
import type { VerbatraMessageArguments, VerbatraMessageKey } from "./verbatra-types.js";

declare function t<Key extends VerbatraMessageKey>(
  key: Key,
  args: VerbatraMessageArguments<Key>,
): string;

t("greeting", { name: "Ada" }); // fine
t("greting", { name: "Ada" });  // error: not a key in the catalog
t("greeting", {});              // error: the name argument is required
t("app.title", { name: "Ada" }); // error: this message takes no argument
```

### Was behauptet wird und was nicht [#was-behauptet-wird-und-was-nicht]

Die Keys sind genau das, was der Format-Adapter erzeugt hat, in Dokumentreihenfolge, deshalb erzeugt derselbe Katalog immer dieselben Bytes. Die Argumente stammen aus den Platzhalter-Tokens, die der Adapter extrahiert hat; Nachrichten in `next-intl-json` und `arb` werden mit dem ICU-Parser des Adapters gelesen, sodass auch ein Argument deklariert wird, das nur einige Zweige verwenden. Geraten wird nichts, und nichts wird als `any` deklariert.

| Deine Nachricht | Was deklariert wird |
| --- | --- |
| keine Platzhalter | `VerbatraNoArguments`, jedes übergebene Argument ist also ein Typfehler |
| benannte Platzhalter (`{{name}}`, `{name}`, `%(name)s`) | eine Objektform mit genau diesen Namen |
| nummerierte oder anonyme Platzhalter (`{0}`, `%@`, `%1$d`) | ein `readonly`-Tupel, eine Position pro Argument |
| ein Argument, das nur einige `select`- oder `plural`-Zweige verwenden (`next-intl-json`, `arb`) | eine optionale Eigenschaft, `name?:`, oder eine optionale Tupel-Position am Ende (`[number, VerbatraArgument?]`); eines, das jeder Zweig verwendet, bleibt Pflicht |
| ein ICU-Argument vom Typ `number`, `plural` oder `selectordinal` in `next-intl-json` oder `arb` | `number` |
| ein ICU-Argument vom Typ `date` oder `time` (`{d, date, short}`) in `next-intl-json` oder `arb` | `Date \| number` |
| ein ICU-Argument vom Typ `select` (`{gender, select, ...}`) in `next-intl-json` oder `arb` | `string` |
| ein i18next-Formatter `number` (`{{count, number}}`) in `i18next-json`, `ngx-translate-json` oder `yaml` | `number` |
| ein i18next-Formatter `datetime` (`{{d, datetime}}`) in denselben drei Formaten | `Date \| number` |
| eine unescapte i18next-Interpolation (`{{- name}}`) | der Name hinter dem Präfix, `name` |
| ein MessageFormat-Argument vom Typ `number`, `plural` oder `choice` (`{0,number}`) in `properties` | `number` |
| eine numerische printf-Konvertierung (`%d`, `%1$d`) | `number` |
| eine printf-String-Konvertierung (`%s`, `%(name)s`) | `string` |
| jeder andere Platzhalter, auch ein schlichtes ICU-Argument (`{name}`) | `VerbatraArgument`, ein Alias für `string \| number` |
| ungültige Nachrichtensyntax, die nur `next-intl-json` und `arb` prüfen | `VerbatraUnknownArguments`, und der Key wird in der Ausgabe genannt |
| eine Nachricht, die benannte, nummerierte und anonyme Argumente mischt | `VerbatraUnknownArguments`, und der Key wird in der Ausgabe genannt |
| ein nummeriertes Argument unter Position 0 oder ab Position 64 (`%0$s`, `{64}`) | `VerbatraUnknownArguments`, und der Key wird in der Ausgabe genannt |

Ein Format, dessen Platzhalter keinen Namen tragen (Apple `.strings`, Android `strings.xml`, positionale gettext-Konvertierungen), bekommt ein Tupel, nie erfundene Namen. Ein Name, der mit verschiedenen Typen vorkommt, bekommt deren Vereinigung: `{d, date, short}` neben einem schlichten `{d}` wird zu `Date | number | string`. Keys sind immer String-Literale in Anführungszeichen, ein Key mit Punkt, reserviertem Wort, führender Ziffer oder Anführungszeichen wird also wortwörtlich deklariert. Ein Key, den der Adapter von der Übersetzung ausgeschlossen hat (ein Blatt, das kein String ist, eine Android-Ressource mit `translatable="false"`), wird nie deklariert und in der Ausgabe genannt.

### Ein Key, dessen eigener Name einen Punkt enthält [#ein-key-dessen-eigener-name-einen-punkt-enthält]

Der deklarierte Key wird so geschrieben, wie verbatra ihn in `verbatra.lock.json` schreibt, nicht in einer Lookup-Syntax, die deine i18n-Bibliothek versteht. Ein JSON-Katalog mit `{"a.b": "..."}` trägt den Key `a\.b`, bewusst verschieden vom verschachtelten Pfad `a.b`:

```ts
export interface VerbatraMessages {
  "a\\.b": VerbatraNoArguments;
}
```

i18next kennt kein Escape für sein Key-Trennzeichen, `t("a\\.b")` schlägt diesen Key also nicht nach. Trägt dein Katalog solche Keys, setz `keySeparator` auf ein anderes Zeichen (oder bei einem flachen Katalog auf `false`) und bilde den deklarierten Key in deinem eigenen typisierten Wrapper auf den Laufzeit-Key ab.

### Der eine Fall, in dem die Deklaration falsch statt nur still ist [#der-eine-fall-in-dem-die-deklaration-falsch-statt-nur-still-ist]

Nur `next-intl-json` und `arb` prüfen die Nachrichtensyntax. In jedem anderen Format liefert ein kaputter Wert wie `"Hello {name"` kein Platzhalter-Token. Die Nachricht wird deshalb als argumentlos deklariert, und das Argument zu übergeben, das sie wirklich braucht, ist ein Compile-Fehler. Gemeldet wird das nicht, weil es nicht erkannt wurde. Reparier den Katalog, nicht die Deklaration.

### Plural-Keys [#plural-keys]

Nichts wird zusammengefasst. i18next trägt den Plural als Key-Suffix, deshalb werden `cart.item_one` und `cart.item_other` so deklariert, wie sie geschrieben sind, und daneben der Basis-Key `cart.item`, den `t("cart.item", { count })` nachschlägt; er verlangt `count` und jedes Argument, das eine der Formen nimmt. Auch die eigenen Typen von i18next akzeptieren beide Schreibweisen. Eine Ordinalgruppe wie `place_ordinal_one` deklariert `place`, und ein Basis-Key, den der Katalog schon enthält, behält seine eigene Deklaration. `VerbatraPluralMessageKey` listet die Keys, die der Adapter als Träger von Pluralformen markiert hat, also die mit Suffix, damit du darauf einschränken kannst.

### Wohin die Deklaration geht [#wohin-die-deklaration-geht]

Standardmäßig landet sie als `verbatra-types.d.ts` im Wurzelverzeichnis deines Projekts. **Committe sie**: Eine Kollegin oder ein CI-Job typprüft dagegen, ohne verbatra laufen zu lassen, und `--check` braucht eine committete Datei zum Vergleichen. Nichts trägt sie in `.gitignore` ein.

Ein Ausgabepfad wird mit `TYPES_OUTPUT_CONFLICT` zurückgewiesen, wenn er:

- keine Datei benennt, auch wenn er auf ein Pfadtrennzeichen endet wie `generated/` (ein leeres `--out` scheitert früher, mit `INVALID_OUT`),
- absolut ist,
- mit `..` aus der Projektwurzel herausklettert,
- keine TypeScript-Datei ist, sein Name also nicht auf `.ts`, `.mts` oder `.cts` endet,
- eine konfigurierte Locale-Datei benennt, Quelle oder Ziel, damit die Generierung nie einen Katalog überschreibt,
- die Lock-Datei `verbatra.lock.json` ist,
- die Herkunftsdatei `verbatra.provenance.json` ist,
- der Übersetzungsspeicher-Cache `verbatra.cache.json` ist,
- eine Datei ist, in der verbatra nach seiner Konfiguration sucht: `package.json`, `.verbatrarc`, `.verbatrarc.json`, `.verbatrarc.yaml`, `.verbatrarc.yml`, `.verbatrarc.js`, `.verbatrarc.cjs`, `.verbatrarc.ts`, `verbatra.config.js`, `verbatra.config.cjs` oder `verbatra.config.ts`,
- die Konfigurationsdatei ist, die der Lauf tatsächlich geladen hat, auch eine, die du mit `--config` angegeben hast,
- die Glossardatei ist, auf die `glossary` in deiner Konfiguration zeigt,
- eine bestehende Datei ist, die nicht mit der Kopfzeile `// Generated by verbatra from` beginnt, die verbatra an den Anfang jeder Deklaration schreibt (eine vorangestellte Byte-Order-Mark und Leerzeilen werden ignoriert), oder zu groß ist, um das zu prüfen.

Dateinamen werden ohne Rücksicht auf Groß- und Kleinschreibung verglichen, und die Prüfungen laufen nach dem Auflösen symbolischer Links noch einmal, sodass ein verlinktes Verzeichnis die Deklaration weder aus der Projektwurzel hinaus noch auf eine geschützte Datei tragen kann. Die letzte Prüfung läuft nur, wenn ein Lauf schreiben würde, und lässt deine Datei unangetastet; `--check` weist eine bestehende Datei nie zurück. Ein Pfad, der alle Prüfungen besteht, kann beim Schreiben trotzdem scheitern (ein schreibgeschütztes Verzeichnis, eine volle Platte): Der Lauf bricht dann mit `TYPES_UNWRITABLE` ab und nennt die Datei und den Dateisystem-Code, etwa `EACCES`.

### Ehrlich bleiben in der CI [#ehrlich-bleiben-in-der-ci]

Lass `types --check` im selben Job neben [`verbatra check`](https://verbatra.kreitz-webdev.de/de/docs/cli/check.md) laufen: `check` fängt Locales ab, die hinter der Quelle zurückbleiben, `types --check` deine Typen. Der Vergleich geht Byte für Byte, und die Deklaration wird immer mit Line-Feed-Zeilenenden geschrieben. Leg die Zeilenenden deshalb in `.gitattributes` fest, sonst lässt ein Checkout, der sie umschreibt, die Datei dauerhaft veraltet zurück:

```text
verbatra-types.d.ts text eol=lf
```

### JSON-Ausgabe [#json-ausgabe]

Schema: [`types-envelope.json`](https://verbatra.kreitz-webdev.de/schema/v1/types-envelope.json) validiert den ganzen Envelope; [Schemas](https://verbatra.kreitz-webdev.de/de/docs/cli/output.md#schemas) listet die übrigen auf.

Mit `--json` trägt der [Envelope](https://verbatra.kreitz-webdev.de/de/docs/cli/output.md#json-ausgabe) ein Ergebnis:

```ts
interface GenerateTypesResult {
  path: string;               // absolute path of the declaration file
  sourcePath: string;         // the source catalog, relative to the project root
  keys: number;               // how many keys are declared
  withArguments: number;      // keys taking at least one determined argument
  unresolved: { key: string; reason: "invalid-message-syntax" | "mixed-argument-styles" | "argument-index-out-of-range" }[];
  excluded: string[];         // keys the adapter excluded, never declared
  plural: string[];           // keys carrying plural forms
  written: boolean;           // false under --check and when the file already matched
  stale: boolean;             // the file on disk differed when the run started
  missing: boolean;           // no file existed when the run started
  check: boolean;             // whether this was a --check run
}
```

### Rückmeldung im Terminal [#rückmeldung-im-terminal]

<AvailableFrom version="0.12.0" />

Die Zusammenfassung geht auf stdout; eine Spinner-Zeile und nach dem Schreiben ein Hinweis, die Datei zu committen, gehen auf stderr (`--check` schlägt nichts vor, und `--quiet` lässt beides weg). Ein erzeugender Lauf, ein unveränderter zweiter Lauf und ein veraltetes `--check`:

```text output
verbatra: generating the declarations... done (0.2s)
verbatra types
  214 keys declared, 63 of them taking arguments, from locales/en.json
  arguments not determined (1):
    legal.notice  invalid-message-syntax
  excluded by the adapter (1): meta.version
  wrote verbatra-types.d.ts
next: commit verbatra-types.d.ts (verbatra types --check compares against it in CI)

verbatra types
  214 keys declared, 63 of them taking arguments, from locales/en.json
  unchanged verbatra-types.d.ts

verbatra types
  215 keys declared, 63 of them taking arguments, from locales/en.json
  verbatra-types.d.ts is out of date, re-run verbatra types
```

Farbe, Spinner und die Variablen dazu findest du unter [Terminalausgabe](https://verbatra.kreitz-webdev.de/de/docs/cli/output.md#terminalausgabe).

## Exit-Codes [#exit-codes]

| Code | Bedeutung |
| --- | --- |
| `0` | die Deklaration wurde erzeugt, egal ob sich die Datei geändert hat; oder `--check` hat sie als aktuell befunden |
| `1` | `--check` hat die committete Deklaration als veraltet oder fehlend befunden (`is missing, run verbatra types to create it`) |
| `2` | konnte nicht laufen: ein Verwendungsfehler, ein Problem mit Konfiguration oder Quelle, ein zurückgewiesener Ausgabepfad (`TYPES_OUTPUT_CONFLICT`) oder eine Deklaration, die nicht geschrieben werden konnte (`TYPES_UNWRITABLE`) |

Den Vertrag, dem jeder Befehl folgt, findest du unter [Exit-Codes](https://verbatra.kreitz-webdev.de/de/docs/cli/output.md#exit-codes).

## Siehe auch [#siehe-auch]

- [`verbatra extract`](https://verbatra.kreitz-webdev.de/de/docs/cli/extract.md) trägt die Keys, die dein Code benutzt, in den Katalog ein; `types` deklariert sie dann.
- [`verbatra check`](https://verbatra.kreitz-webdev.de/de/docs/cli/check.md) ist das CI-Gate für Locale-Drift, direkt neben diesem für Typ-Drift.
- [Formate](https://verbatra.kreitz-webdev.de/de/docs/formats.md) listet die Platzhaltersyntax jedes Formats.
- [Exit-Codes und JSON-Ausgabe](https://verbatra.kreitz-webdev.de/de/docs/cli/output.md) behandelt den Envelope und den Exit-Code-Vertrag.
