---
title: "verbatra types"
description: "Generate TypeScript declarations for your catalog keys and their message arguments, without calling a provider or reading an API key."
type: "reference"
---
# verbatra types

Generate TypeScript declarations for your catalog keys and their message arguments, without calling a provider or reading an API key.

<AvailableFrom version="0.11.0" />

Turn a misspelled key or a forgotten interpolation argument into a compile error. `types` reads your source catalog through the configured [format adapter](https://verbatra.kreitz-webdev.de/docs/formats.md) and writes one declaration file: a union of every key and, per key, the arguments its message takes. It calls no provider, reads no API key, and never touches a target locale file.

## Synopsis [#synopsis]

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

## Examples [#examples]

```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 | Default | Effect |
| --- | --- | --- | --- |
| `--out` | `<path>` | `verbatra-types.d.ts` | where to write the declaration, relative to the working directory and inside the project root; missing directories are created |
| `--check` | none | off | compare the committed declaration with a fresh generation byte for byte, writing nothing; exit `1` when it is out of date or missing |

Also takes the [global flags](https://verbatra.kreitz-webdev.de/docs/cli.md#global-flags) `--cwd`, `--config` and `--json`.

## Behavior [#behavior]

### What it generates [#what-it-generates]

Given a `locales/en.json` like this:

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

you get:

```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";
```

Type your own translation function against it and the compiler does the rest:

```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
```

### What it will and will not claim [#what-it-will-and-will-not-claim]

The keys are exactly what the format adapter produced, in document order, so the same catalog always produces the same bytes. Arguments come from the placeholder tokens the adapter extracted; `next-intl-json` and `arb` messages are read with the adapter's ICU parser, so an argument only some branches use is still declared. Nothing is guessed, and nothing is declared as `any`.

| Your message | What is declared |
| --- | --- |
| no placeholders | `VerbatraNoArguments`, so passing anything is a type error |
| named placeholders (`{{name}}`, `{name}`, `%(name)s`) | an object shape with those names |
| numbered or anonymous placeholders (`{0}`, `%@`, `%1$d`) | a `readonly` tuple, one position per argument |
| an argument only some `select` or `plural` branches use (`next-intl-json`, `arb`) | an optional member, `name?:`, or an optional tuple position at the end (`[number, VerbatraArgument?]`); one every branch uses stays required |
| an ICU `number`, `plural` or `selectordinal` argument in `next-intl-json` or `arb` | `number` |
| an ICU `date` or `time` argument (`{d, date, short}`) in `next-intl-json` or `arb` | `Date \| number` |
| an ICU `select` argument (`{gender, select, ...}`) in `next-intl-json` or `arb` | `string` |
| an i18next `number` formatter (`{{count, number}}`) in `i18next-json`, `ngx-translate-json` or `yaml` | `number` |
| an i18next `datetime` formatter (`{{d, datetime}}`) in the same three formats | `Date \| number` |
| an i18next unescaped interpolation (`{{- name}}`) | the name behind the prefix, `name` |
| a MessageFormat `number`, `plural` or `choice` argument (`{0,number}`) in `properties` | `number` |
| a numeric printf conversion (`%d`, `%1$d`) | `number` |
| a printf string conversion (`%s`, `%(name)s`) | `string` |
| any other placeholder, including a plain ICU argument (`{name}`) | `VerbatraArgument`, an alias for `string \| number` |
| invalid message syntax, which only `next-intl-json` and `arb` check for | `VerbatraUnknownArguments`, and the key is named in the output |
| a message that mixes named, numbered and anonymous arguments | `VerbatraUnknownArguments`, and the key is named in the output |
| a numbered argument below position 0 or at position 64 or higher (`%0$s`, `{64}`) | `VerbatraUnknownArguments`, and the key is named in the output |

A format whose placeholders carry no name (Apple `.strings`, Android `strings.xml`, positional gettext conversions) gets a tuple, never invented names. A name used with different types gets the union of them: `{d, date, short}` next to a plain `{d}` becomes `Date | number | string`. Keys are always quoted string literals, so a key with a dot, a reserved word, a leading digit or a quote is declared verbatim. A key the adapter excluded from translation (a non-string leaf, an Android `translatable="false"` resource) is never declared and is named in the output.

### A key whose own name contains a dot [#a-key-whose-own-name-contains-a-dot]

The declared key is spelled the way verbatra spells it in `verbatra.lock.json`, not in a lookup syntax your i18n library understands. A JSON catalog holding `{"a.b": "..."}` carries the key `a\.b`, deliberately distinct from the nested path `a.b`:

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

i18next has no escape for its key separator, so `t("a\\.b")` does not look up that key. If your catalog carries such keys, set `keySeparator` to another character (or `false` for a flat catalog) and map the declared key to the runtime key in your own typed wrapper.

### The one case where the declaration is wrong rather than merely silent [#the-one-case-where-the-declaration-is-wrong-rather-than-merely-silent]

Only `next-intl-json` and `arb` validate message syntax. In every other format, a broken value such as `"Hello {name"` yields no placeholder token, so the message is declared as taking no arguments and passing the argument it really needs is a compile error. Nothing reports it, because nothing detected it. Fix the catalog, not the declaration.

### Plural keys [#plural-keys]

Nothing is collapsed. i18next carries plural as a key suffix, so `cart.item_one` and `cart.item_other` are declared as written, and next to them the base key `cart.item` that `t("cart.item", { count })` looks up, requiring `count` and every argument any form takes. i18next's own typings accept both spellings too. An ordinal group such as `place_ordinal_one` declares `place`, and a base key the catalog already holds keeps its own declaration. `VerbatraPluralMessageKey` lists the keys the adapter marked as carrying plural forms, the suffixed ones, so you can narrow to them.

### Where the declaration goes [#where-the-declaration-goes]

By default it lands at `verbatra-types.d.ts` in your project root. **Commit it**: a teammate or a CI job type-checks against it without running verbatra, and `--check` needs a committed file to compare against. Nothing adds it to `.gitignore`.

An output path is refused with `TYPES_OUTPUT_CONFLICT` when it:

- names no file, including one that ends in a path separator such as `generated/` (an empty `--out` fails earlier, with `INVALID_OUT`),
- is absolute,
- climbs out of the project root with `..`,
- is not a TypeScript file, meaning its name does not end in `.ts`, `.mts` or `.cts`,
- names a configured locale file, source or target, so generation can never overwrite a catalog,
- is the lock file, `verbatra.lock.json`,
- is the provenance file, `verbatra.provenance.json`,
- is the translation-memory cache, `verbatra.cache.json`,
- is a file verbatra searches for its configuration: `package.json`, `.verbatrarc`, `.verbatrarc.json`, `.verbatrarc.yaml`, `.verbatrarc.yml`, `.verbatrarc.js`, `.verbatrarc.cjs`, `.verbatrarc.ts`, `verbatra.config.js`, `verbatra.config.cjs` or `verbatra.config.ts`,
- is the config file the run actually loaded, including one you named with `--config`,
- is the glossary file your config points `glossary` at,
- is an existing file that does not begin with the `// Generated by verbatra from` header line verbatra writes at the top of every declaration (a leading byte order mark and blank lines are ignored), or is too large to verify.

File names are compared regardless of case, and the checks run again after symbolic links are resolved, so a linked directory cannot carry the declaration outside the project root or onto a protected file. The last check runs only when a run would write, and leaves your file untouched; `--check` never refuses an existing file. A path that passes every check can still fail while writing (a read-only directory, a full disk): the run stops with `TYPES_UNWRITABLE`, naming the file and the file-system code such as `EACCES`.

### Keeping it honest in CI [#keeping-it-honest-in-ci]

Run `types --check` beside [`verbatra check`](https://verbatra.kreitz-webdev.de/docs/cli/check.md) in the same job: `check` catches locales drifting behind the source, `types --check` catches your types drifting behind it. The comparison is byte for byte and the declaration is always written with line-feed endings, so pin the endings in `.gitattributes`, or a checkout that rewrites them leaves the file permanently out of date:

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

### JSON output [#json-output]

Schema: [`types-envelope.json`](https://verbatra.kreitz-webdev.de/schema/v1/types-envelope.json) validates the whole envelope; [Schemas](https://verbatra.kreitz-webdev.de/docs/cli/output.md#schemas) lists the others.

With `--json`, the [envelope](https://verbatra.kreitz-webdev.de/docs/cli/output.md#json-output) carries one result:

```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
}
```

### Terminal feedback [#terminal-feedback]

<AvailableFrom version="0.12.0" />

The summary goes to stdout; a spinner line and, after a write, a hint to commit the file go to stderr (`--check` suggests nothing, and `--quiet` drops both). A generating run, an unchanged re-run and a stale `--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
```

See [Terminal output](https://verbatra.kreitz-webdev.de/docs/cli/output.md#terminal-output) for color, the spinner and the variables that control them.

## Exit codes [#exit-codes]

| Code | Meaning |
| --- | --- |
| `0` | the declaration was generated, whether or not the file changed; or `--check` found it current |
| `1` | `--check` found the committed declaration out of date, or missing (`is missing, run verbatra types to create it`) |
| `2` | could not run: a usage error, a config or source problem, a refused output path (`TYPES_OUTPUT_CONFLICT`), or a declaration that could not be written (`TYPES_UNWRITABLE`) |

See [Exit codes](https://verbatra.kreitz-webdev.de/docs/cli/output.md#exit-codes) for the contract every command follows.

## Related [#related]

- [`verbatra extract`](https://verbatra.kreitz-webdev.de/docs/cli/extract.md) adds the keys your code uses to the catalog; `types` then declares them.
- [`verbatra check`](https://verbatra.kreitz-webdev.de/docs/cli/check.md) is the CI gate for locale drift, next to this one for type drift.
- [Formats](https://verbatra.kreitz-webdev.de/docs/formats.md) lists the placeholder syntax each format uses.
- [Exit codes and JSON output](https://verbatra.kreitz-webdev.de/docs/cli/output.md) covers the envelope and the exit-code contract.
