API reference@evolu/commonType › templateLiteralParser

function templateLiteralParser<Parts>(
  ...parts: {
    readonly [Index in string | number | symbol]: ValidateTemplateLiteralPart<
      Parts[Index]
    >;
  } & TemplateLiteralValidation<Parts>
): TemplateLiteralParserType<Parts>;

Defined in: packages/common/src/Type.ts:4155

Template literal Type that parses canonical strings into Tuples.

Accepts the same template parts as templateLiteral: fixed string literals and Types canonically encoded as strings. Instead of keeping Output as a string, fixed literals define the framing and Output is a readonly Tuple of the decoded Type parts. to encodes that Tuple back into the canonical string represented by the parent Type. At least one Type part is required.

When every capture uses identity encoding, the parent Output is the exact TypeScript template literal type. A transforming capture makes it nominal; create such strings with to or validate them with the parent Type.

Deterministic framing is a core correctness guarantee. It preserves reversibility and keeps capture boundaries unambiguous. Different capture Tuples must never encode to the same string. The parser provides predictable parsing without pathological backtracking and decodes each capture once, so adversarial input cannot trigger exponential parser work. Fixed-width captures may be adjacent, but only one variable-width capture is allowed. Declarations that could join UTF-16 surrogate halves across parts are rejected during construction.

Keep capture unions reasonably small to avoid excessive compiler work.

TypeScript template literal types can describe a fixed number of digit positions, but not an arbitrarily long sequence of digits. Such grammars use branded Types such as DecimalString; templateLiteralParser preserves that exactness by requiring a validated branded capture when encoding.

Example

A template literal Type defines both a canonical string representation and the structured data decoded from it:

import {
  assertFalse,
  assertEqual,
  assertErr,
  assertOk,
  assertType,
  Data,
  templateLiteralParser,
  union,
} from "@evolu/common";

const Language = union("en", "cs");
const Region = union("US", "CZ");

// Define a Type for "en-US" | "en-CZ" | "cs-US" | "cs-CZ".
const SupportedLocale = templateLiteralParser(Language, "-", Region);

// Output is the decoded language and region.
type SupportedLocale = typeof SupportedLocale.Output;
assertType<readonly ["en" | "cs", "US" | "CZ"], SupportedLocale>();

// The parent Output is the canonical locale string.
type SupportedLocaleLiteral = typeof SupportedLocale.parent.Output;
assertType<"en-US" | "en-CZ" | "cs-US" | "cs-CZ", SupportedLocaleLiteral>();

// Parse an unknown string into structured data.
const result = SupportedLocale.fromUnknown("cs-CZ");
assertOk(result, ["cs", "CZ"]);
const locale = result.value;
assertType<SupportedLocale, typeof locale>();
const invalid = SupportedLocale.fromUnknown("cs/CZ");
assertErr(invalid);
assertType(Data, invalid.error);
const error: Data = invalid.error;
assertEqual(error, {
  type: "TemplateLiteral",
  value: "cs/CZ",
});

// Encode structured data into its canonical string.
const localeLiteral = SupportedLocale.to(locale);
assertType<SupportedLocaleLiteral, typeof localeLiteral>();
assertEqual(localeLiteral, "cs-CZ");

// Validate a string configuration value.
const configValue: unknown = "cs-CZ";
assertType(SupportedLocale.parent, configValue);
assertType<SupportedLocaleLiteral, typeof configValue>();
assertFalse(SupportedLocale.parent.is("fr-CZ"));

SupportedLocale is structured data for application code. SupportedLocaleLiteral is its canonical representation for configuration and other APIs that require a string, such as URL parameters, environment variables, and storage keys.

Use branded captures for strings that TypeScript template literal types cannot express exactly, such as arbitrary-length canonical decimals:

import {
  assertEqual,
  assertOk,
  NonNegativeDecimalString,
  templateLiteralParser,
} from "@evolu/common";

const DecimalText = templateLiteralParser("decimal:", NonNegativeDecimalString);

// DecimalText.to requires a validated NonNegativeDecimalString.
const zero = NonNegativeDecimalString.orThrow("0");

assertOk(DecimalText.fromUnknown("decimal:0"), [zero]);
assertEqual(DecimalText.to([zero]), "decimal:0");

Capture Types (the Type arguments passed to templateLiteralParser) can use transformations to decode substrings into non-string data:

import {
  assertEqual,
  assertOk,
  assertType,
  Int64FromInt64String,
  templateLiteralParser,
} from "@evolu/common";

const ItemId = templateLiteralParser("item-", Int64FromInt64String);
type ItemId = typeof ItemId.Output;
type ItemIdLiteral = typeof ItemId.parent.Output;

// Decode the string into structured data.
const result = ItemId.fromUnknown("item-42");
assertOk(result, [42n]);
const itemId = result.value;
assertType<ItemId, typeof itemId>();

// Encode the structured data into its canonical string.
const itemIdLiteral = ItemId.to(itemId);
assertType<ItemIdLiteral, typeof itemIdLiteral>();
assertEqual(itemIdLiteral, "item-42");

// TypeScript cannot prove from the literal alone that "42" is a valid Int64 encoding.
// @ts-expect-error Validate it with ItemId.parent or create it with ItemId.to.
const _invalidItemIdLiteral: ItemIdLiteral = "item-42";

Fixed-width captures can be adjacent:

import {
  assertEqual,
  assertOk,
  templateLiteralParser,
  union,
} from "@evolu/common";

const Digit = union("0", "1", "2", "3", "4", "5", "6", "7", "8", "9");
const TwoDigits = templateLiteralParser(Digit, Digit);
type TwoDigits = typeof TwoDigits.Output;
type TwoDigitsLiteral = typeof TwoDigits.parent.Output;

const twoDigits: TwoDigits = ["4", "2"];
const twoDigitsLiteral: TwoDigitsLiteral = "42";
// @ts-expect-error TwoDigitsLiteral requires exactly two digits.
const _threeDigitsLiteral: TwoDigitsLiteral = "123";

assertOk(TwoDigits.from.parent(twoDigitsLiteral), twoDigits);
assertEqual(TwoDigits.to(twoDigits), twoDigitsLiteral);

TypeScript rejects multiple variable-width captures because their encoded boundaries would be ambiguous:

import { String, templateLiteralParser } from "@evolu/common";

// @ts-expect-error At most one Type capture can have a variable-width string representation.
templateLiteralParser(String, ":", String);

This restriction keeps encoding reversible: different capture Tuples must never produce the same string. A delimiter alone is not enough because it can also occur inside a capture. Some formats could provide stronger guarantees, such as captures that exclude a delimiter; support for those can be added when concrete use cases justify the additional framing rules.