Typorithm · hard

6. Template literal types and bounded vocabularies

typescriptstable-typescriptinterviewindependent-practice
Saved on this device
Full lesson · 4 learning steps Open any exercise for focused practice
1

Step 1 of 4 · intro card

6. Template literal types and bounded vocabularies

0%

Concept and contract

Encode useful naming relationships without constructing unmanageable type combinations.

Template literal types combine literal strings and unions into new string vocabularies. A route name, event name or CSS variant can stay connected to a finite source domain instead of becoming an arbitrary string. Intrinsic helpers such as Uppercase and Capitalize transform that vocabulary statically.

Keep the domain bounded. Combining unions produces combinations, so several large dimensions can create expensive and unreadable types. A small hand-authored union or generated declaration may be better than an elaborate compile-time expansion when the vocabulary is large.

Mechanism and worked example

The example constructs event names from two fields and a Changed suffix.

The example constructs event names from two fields and a Changed suffix. A receiving function accepts only those names. This catches typos early while keeping the naming rule close to the field vocabulary it derives from.

A template literal type does not prove that an arbitrary runtime string matches its pattern. Parsing a URL or event from outside the program still requires checks. Broad patterns involving string or number can admit more values than a business rule such as a valid identifier or a bounded integer.

Complete worked example

typescript

type Field = "title" | "count";
type EventName = `${Field}Changed`;
function eventName(name: EventName): EventName { return name; }
const event = eventName("titleChanged");
Reasoning
The suffix stays connected to the finite field union without creating runtime handlers.

Counterexamples and common mistakes

The rejected name has the wrong field spelling.

The rejected name has the wrong field spelling. Casting a user-supplied string to EventName removes the diagnostic but cannot tell whether an event handler exists. Store runtime handlers in a registry whose keys actually correspond to the accepted names.

Do not infer JavaScript string indexing behavior from type-level string decomposition without checking the compiler version. TypeScript 7 handles inferred template-literal Unicode code points differently from earlier UTF-16-oriented behavior; avoid teaching an ASCII shortcut as a general Unicode character counter.

Rejected counterexample: expected compiler diagnostic

typescript

type EventName = `${"title" | "count"}Changed`;
// @ts-expect-error misspelled field name.
const invalid: EventName = "titelChanged";
Reasoning
The finite vocabulary rejects a misspelling before any dispatch occurs.

Independent practice

Create a finite message key vocabulary from the languages en and uk and the labels title and empty.

Create a finite message key vocabulary from the languages en and uk and the labels title and empty. Start by writing the four expected keys, then replace the repetition with one template literal type. Implement a complete message table checked against that vocabulary.

Reject a misspelled key and a missing translation. Optional extension: add a third language and observe which missing entries the compiler reports. Do not add a runtime translation fallback until you have deliberately defined its behavior for missing external keys.

Step 1: List the four translation keys. Step 2: Require them with a finite Record.

Practice starter: solve before reading the solution

typescript

type Language = "en" | "uk";
type Label = "title" | "empty";
// Write four keys first, then derive their union.
type MessageKey = unknown;
Reasoning
Enumerate the four combinations manually to understand what the abstraction must preserve.
  • Hint 1: Template unions generate combinations.
  • Hint 2: The object, not the type, supplies translations.

Solution, tests and trade-offs

The solution uses Record<MessageKey, string> to require all four combinations.

The solution uses Record<MessageKey, string> to require all four combinations. Template expansion defines the static vocabulary; the object literal supplies actual strings. This separation makes it easy to explain why a type declaration alone cannot create translations.

Test table lookups for both languages. Compile a negative key use and temporarily remove one required entry to confirm completeness checking. The small vocabulary is intentionally practical; the exercise develops a relationship without requiring a large string-manipulation puzzle.

Practice solution and acceptance checks

typescript

type Language = "en" | "uk";
type Label = "title" | "empty";
type MessageKey = `${Language}.${Label}`;
const messages: Record<MessageKey, string> = {
  "en.title": "Roles", "en.empty": "No roles",
  "uk.title": "Вакансії", "uk.empty": "Немає вакансій"
};

// Acceptance checks
if(messages["en.empty"]!=="No roles"||messages["uk.title"]!=="Вакансії")throw new Error("translations");
Reasoning
Record requires every generated key, while the object supplies the actual translations.

Interview reasoning and next step

For interviews, derive the Cartesian combinations of two tiny unions and explain the cost of adding more dimensions.

For interviews, derive the Cartesian combinations of two tiny unions and explain the cost of adding more dimensions. Distinguish pattern acceptance from runtime parsing, especially when the template contains a broad string placeholder.

Next, use built-in utility types to express common transformations before writing custom machinery. Familiar standard helpers often communicate intent better than a clever expression with equivalent behavior.

Optional stretch — after the core task works: Add one language and inspect the missing entries.

Encode useful naming relationships without constructing unmanageable type combinations.