0Pricing
TypeScript Academy · Lesson

Typing Translation Keys

Derive a union of valid keys from message files.

Typing Translation Keys is a free TypeScript Academy lesson on CoddyKit — lesson 1 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the TypeScript Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

Deriving Valid Translation Keys

In a type-safe i18n setup, the set of valid translation keys is derived from your messages object. A typo like t("greetng") becomes a compile error, not a missing-string bug at runtime.

A Messages Object

Start with a plain object of messages. It is the single source of truth for both values and the key type.

const messages = {
  greeting: "Hello",
  farewell: "Goodbye",
  cart: {
    empty: "Your cart is empty",
    checkout: "Proceed to checkout",
  },
} as const;

Top-Level Keys With keyof

For a flat object, keyof typeof gives the union of keys.

type TopKeys = keyof typeof messages;
// "greeting" | "farewell" | "cart"

The Nested Problem

Real message catalogs are nested. We want dotted paths like "cart.empty", not just top-level keys. We need a recursive type that walks the object.

A Recursive Path Type

A conditional + mapped type builds dotted paths by recursing into nested objects and prefixing keys. The prefix uses a template literal type, written with backticks in real code as [backtick]${K}.${Paths<T[K]>}[backtick]. We show it below as JoinPath(K, child) to keep the listing clean.

type JoinPath<K extends string, Rest extends string> = K + "." + Rest;
// (Real TS uses a template literal type:
//   the K dot Rest pattern delimited by backticks.)

type Paths<T> = {
  [K in keyof T & string]: T[K] extends string
    ? K
    : K | (T[K] extends object
        ? JoinPath<K, Paths<T[K]>>
        : never);
}[keyof T & string];

Applying It

Feeding the messages type to Paths yields every valid dotted key.

type MsgKey = Paths<typeof messages>;
// "greeting" | "farewell" | "cart" | "cart.empty" | "cart.checkout"

Leaves-Only Keys

Often you only want the leaf paths (actual strings), excluding intermediate objects. Adjust the recursion to skip non-string nodes. The K-dot-child prefix again uses a template literal type (backtick-delimited in real code), shown here as JoinPath.

type LeafPaths<T> = T extends string
  ? ""
  : {
      [K in keyof T & string]: T[K] extends string
        ? K
        : JoinPath<K, LeafPaths<T[K]>>;
    }[keyof T & string];
// "greeting" | "farewell" | "cart.empty" | "cart.checkout"

A Typed t Function

Constrain the translate function parameter to the derived key union so only valid keys compile.

declare function t(key: LeafPaths<typeof messages>): string;

t("cart.empty");   // ok
t("cart.missing"); // Error: not a valid key

Resolving the Value Type

You can go further and infer the value type at a path, useful when messages have non-string entries. A path-indexing conditional type walks the dots. It splits the path with a template literal pattern that, in real code, is written with backticks as the Head dot Rest shape with infer. We denote that split as SplitHead/SplitRest below.

// Conceptually: split "cart.empty" into Head="cart", Rest="empty"
// using a template literal pattern with infer.

type ValueAt<T, P extends string> =
  P extends SplitHead<infer Head, infer Rest>
    ? Head extends keyof T ? ValueAt<T[Head], Rest> : never
    : P extends keyof T ? T[P] : never;

Why as const Matters

Without as const, string values widen to string and you lose literal info needed later (for example, placeholder extraction). Always declare catalogs as const.

Why This Matters

Deriving keys from the catalog means adding, renaming, or removing a message instantly updates the allowed key type everywhere. No central enum to maintain, no stale string constants.

Quick Check

Confirm your understanding of typing translation keys.

Recap

You derived a union of valid keys from a messages object using keyof plus a recursive template-literal path type. Leaf-only variants exclude intermediate objects, a typed t rejects invalid keys, and as const preserves the literal info the type machinery needs.

Frequently asked questions

Is the “Typing Translation Keys” lesson free?

Yes — the full text of “Typing Translation Keys” is free to read here on the web, and the TypeScript Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the TypeScript Academy course, upgrade to CoddyKit PRO.

What will I learn in “Typing Translation Keys”?

Derive a union of valid keys from message files. You practise TypeScript Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start TypeScript Academy?

No prior experience is required. TypeScript Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 1 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Typing Translation Keys” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this TypeScript Academy lesson?

Yes. Every TypeScript Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Typing Translation Keys
  2. Interpolation Type Safety
  3. Pluralization with Types
  4. Locale-Aware Type Inference
← Back to TypeScript Academy