Kodokon kodokon.com

Шаблонные литеральные типы: строки, вычисленные при компиляции

Используй шаблонные литеральные типы, чтобы порождать объединения через декартовы произведения и разбирать строковые литералы с помощью infer.

9 мин · 3 вопросов

Открыть этот урок в Kodokon

Шаблонные литеральные типы переносят синтаксис шаблонных строк в мир типов. Подставь объединение - и компилятор вычислит декартово произведение: каждая возможная комбинация станет членом результата. Это тот инструмент, который позволяет типизировать ключи событий, маршруты или токены дизайн-системы вплоть до символа.

TYPESCRIPT
type Lang = "fr" | "en";
type Theme = "light" | "dark";

type ThemeKey = `${Lang}-${Theme}`;
// "fr-light" | "fr-dark" | "en-light" | "en-dark"
Два подставленных объединения: декартово произведение из 4 членов.

Настоящая мощь раскрывается в связке с infer: шаблонный литеральный тип превращается в образец для сопоставления строк. Компилятор нарезает входной литерал, чтобы совпасть с каждым сегментом; когда возможно несколько разбиений, побеждает первое вхождение разделителя. Добавь рекурсию - и получишь настоящие парсеры, работающие во время компиляции.

TYPESCRIPT
type Split<S extends string, Sep extends string> =
  S extends `${infer Head}${Sep}${infer Rest}`
    ? [Head, ...Split<Rest, Sep>]
    : [S];

type Parts = Split<"a.b.c", ".">;
// ["a", "b", "c"]
Полностью статическое рекурсивное разбиение.

Этот паттерн блистает при извлечении параметров маршрута в стиле Express. Каждый рекурсивный вызов съедает один сегмент :param, а итоговое объединение перечисляет все имена параметров - их затем можно передать в Record, чтобы типизировать сигнатуру обработчика.

TYPESCRIPT
type Params<Path extends string> =
  Path extends `${string}:${infer P}/${infer Rest}`
    ? P | Params<Rest>
    : Path extends `${string}:${infer P}`
      ? P
      : never;

type RouteParams = Params<"/users/:id/posts/:postId">;
// "id" | "postId"
Извлечение параметров маршрута во время компиляции.

TypeScript поставляется с четырьмя встроенными утилитами - Uppercase, Lowercase, Capitalize, Uncapitalize - реализованными прямо внутри компилятора (ключевое слово intrinsic), так что переписать их самому нельзя. Ещё один продакшен-паттерн: string & {}. В объединении string поглотил бы литералы, и редактор потерял бы автодополнение; пересечение с {} создаёт эквивалентный, но отдельный тип, который компилятор не сворачивает.

TYPESCRIPT
type Method = "get" | "post";
type Handler = `on${Capitalize<Method>}`;
// "onGet" | "onPost"

type KnownColor = "red" | "green" | "blue";
type Color = KnownColor | (string & {});

declare function paint(color: Color): void;

paint("red");     // suggested by autocompletion
paint("#ff8800"); // also accepted
Встроенные утилиты и трюк string & {} ради автодополнения.

Проверка знаний

Убедись, что запомнил ключевые моменты этого урока.

  1. Если в A 3 члена, а в B - 4, сколько членов содержит ${A}-${B}?
    • 7
    • 12
    • 1
    • 4
  2. Что даёт Capitalize, применённый к литералу "hello world"?
    • "Hello World"
    • "Hello world"
    • "HELLO WORLD"
  3. Зачем писать KnownColor | (string & {}), а не KnownColor | string?
    • Чтобы запретить строки вне KnownColor
    • Чтобы string не поглотил литералы и автодополнение сохранилось
    • Чтобы улучшить производительность компилятора
    • Обе формы строго эквивалентны