Template Literal Types в TypeScript. Конструктор строковых типов

Frontend

Шаблонные литералы получили незаслуженно мало внимания. Хотя это мощнейший инструмент, который полезен при разработке различного вида кастомизируемых компонентов. Используя и комбинируя шаблоны, можно создавать коллекции строковых типов всего за одну строку.

Предлагаю обсудить эту тему в стиле самих шаблонных литералов — кратко и емко.

Как создать шаблонный литерал

Синтаксис мало чем отличается от привычного нам по JS, только вместо какой-нибудь переменной, мы присваиваем шаблон некоему type

type IPhone = `iPhone ${number}`;

let iphone17: IPhone = 'iPhone 17'; // ok
let pixelPhone: IPhone = 'Pixel '; // ошибка
let iphoneSE: IPhone = 'iPhone SE'; // ошибка

Причем обратите внимание, что TS учитывает тип значения которое является переменной частью строки.

И что со всем этим делать?

Чаще всего это пригождается, когда нужно скомбинировать несколько юнион-типов, чтобы получить наборы допустимых строк

type Color = "red" | "green" | "blue";
type Size = "small" | "medium" | "large";

type ButtonType = `${Color}-${Size}`;
// type ButtonType = "red-small" | "red-medium" | "red-large" | 
//                  "green-small" | "green-medium" | "green-large" | 
//                  "blue-small" | "blue-medium" | "blue-large"

Или, например для позиции какого-нибудь тултипа

type Vertical = "top" | "bottom";
type Horizontal = "left" | "right";

type Position = `${Vertical}-${Horizontal}`;
// "top-left" | "top-right" | "bottom-left" | "bottom-right"

Также в TS есть встроенные ютилити-тайпы

  • Uppercase<StringType>
  • Lowercase<StringType>
  • Capitalize<StringType>
  • Uncapitalize<StringType>

Применяя их вместе с шаблонными литералами и юнион-типами, можно делать такие вещи:

type Route = "home" | "about" | "contact";

type ApiEndpoint = `api/${Uppercase<Route>}`;
// "api/HOME" | "api/ABOUT" | "api/CONTACT"

type CssClass = `btn-${Lowercase<Route>}`;
// "btn-home" | "btn-about" | "btn-contact"

Еще несколько юз-кейсов

1. Типизируем API событий

type HttpMethod = "get" | "post" | "put" | "delete";
type ApiVersion = "v1" | "v2";

type ApiRoute = `/${ApiVersion}/${Uppercase<HttpMethod>}`;
// "/v1/GET" | "/v1/POST" | "/v1/PUT" | "/v1/DELETE" | 
// "/v2/GET" | "/v2/POST" | "/v2/PUT" | "/v2/DELETE"

2. Создаем CSS-in-JS типы

type CssUnit = "px" | "rem" | "em" | "%";
type CssProperty = "margin" | "padding" | "width" | "height";

type CssValue<K extends CssProperty> = 
  K extends "width" | "height" 
    ? `${number}${CssUnit}` 
    : `${number}${CssUnit}` | "auto";

type Styles = {
  [P in CssProperty as `$${P}`]: CssValue<P>;
};
// { $margin: ..., $padding: ..., $width: ..., $height: ... }

3. Валидируем пути

type Path = `/${string}`;
type FilePath = `${Path}.${"ts" | "js" | "json"}`;

function readFile(path: FilePath) {
  // implementation
}

readFile("/src/index.ts"); // ok
readFile("index.js"); // ошибка, т.к. не начинается со слеша
readFile("/config.txt"); // ошибка, т.к. не то расширение

Итого

Используем шаблонные литералы
1. Для валидации строковых форматов
2. Автоматизации генерации наборов строковых типов

Используем с умом и не переусложняем. Желаю успехов!

Симо Мофин
Симо Мофин

Senior Frontend Developer
Главный по блогу